Compare commits

..

2 Commits

Author SHA1 Message Date
Joel Wetzell 1d4cb251e9 switch to WebMIDI driver when building JS 2026-03-30 15:16:35 -05:00
Joel Wetzell 050ada6a70 mess around with support WASM builds 2026-03-30 14:46:35 -05:00
199 changed files with 5869 additions and 7850 deletions
+18 -199
View File
@@ -4,240 +4,59 @@ on:
push: push:
tags: tags:
- "v*" - "v*"
workflow_dispatch:
permissions: permissions:
contents: write contents: write
jobs: jobs:
setup_release: goreleaser:
runs-on: ubuntu-latest runs-on: ubuntu-latest
outputs:
version: ${{ steps.set_version.outputs.version }}
short_commit: ${{ steps.set_commit.outputs.short_commit }}
steps: steps:
- name: checkout - name: checkout
uses: actions/checkout@v7 uses: actions/checkout@v6
with:
fetch-depth: 0
- name: set environment variables
run: echo "SHOWBRIDGE_VERSION=${GITHUB_REF_NAME}" >> $GITHUB_ENV
- name: clean tag
if: startsWith(github.ref, 'refs/tags/v')
run: echo "SHOWBRIDGE_VERSION=${SHOWBRIDGE_VERSION#v}" >> $GITHUB_ENV
- name: set version
id: set_version
run: echo "version=${SHOWBRIDGE_VERSION}" >> $GITHUB_OUTPUT
- name: set commit
id: set_commit
run: echo "short_commit=${GITHUB_SHA::7}" >> $GITHUB_OUTPUT
- name: create release
uses: softprops/action-gh-release@v3
if: github.ref_type == 'tag' && github.event_name != 'workflow_dispatch' && startsWith(github.ref, 'refs/tags/v')
with:
draft: true
generate_release_notes: true
linux_amd64:
runs-on: ubuntu-24.04
needs: setup_release
env:
SHOWBRIDGE_VERSION: ${{ needs.setup_release.outputs.version }}
SHORT_COMMIT: ${{ needs.setup_release.outputs.short_commit }}
steps:
- name: checkout
uses: actions/checkout@v7
with: with:
fetch-depth: 0 fetch-depth: 0
- name: setup go - name: setup go
uses: actions/setup-go@v7 uses: actions/setup-go@v6
with: with:
go-version-file: "go.mod" go-version-file: "go.mod"
- name: install alsa lib - name: release
run: sudo apt-get install -y libasound2-dev uses: goreleaser/goreleaser-action@v7
- name: build
run: |
CGO_ENABLED=1 go build -o showbridge -ldflags="-s -w -X main.version=${SHOWBRIDGE_VERSION}-${SHORT_COMMIT}" ./cmd/showbridge
tar -czf showbridge_${SHOWBRIDGE_VERSION}_linux-amd64.tar.gz showbridge
- name: upload linux artifacts
uses: actions/upload-artifact@v7
if: github.event_name == 'workflow_dispatch'
with: with:
name: showbridge-linux-amd64 workdir: cmd/showbridge
path: | distribution: goreleaser
showbridge_*.tar.gz version: "v2.13.0"
- name: upload artifacts to release args: release --clean
uses: softprops/action-gh-release@v3 env:
if: github.ref_type == 'tag' && github.event_name != 'workflow_dispatch' && startsWith(github.ref, 'refs/tags/v') GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
draft: true
files: showbridge_*.tar.gz
linux_arm64:
runs-on: ubuntu-24.04-arm
needs: setup_release
env:
SHOWBRIDGE_VERSION: ${{ needs.setup_release.outputs.version }}
SHORT_COMMIT: ${{ needs.setup_release.outputs.short_commit }}
steps:
- name: checkout
uses: actions/checkout@v7
with:
fetch-depth: 0
- name: setup go
uses: actions/setup-go@v7
with:
go-version-file: "go.mod"
- name: install alsa lib
run: sudo apt-get install -y libasound2-dev
- name: build
run: |
CGO_ENABLED=1 go build -o showbridge -ldflags="-s -w -X main.version=${SHOWBRIDGE_VERSION}-${SHORT_COMMIT}" ./cmd/showbridge
tar -czf showbridge_${SHOWBRIDGE_VERSION}_linux-arm64.tar.gz showbridge
- name: upload linux artifacts
uses: actions/upload-artifact@v7
if: github.event_name == 'workflow_dispatch'
with:
name: showbridge-linux-arm64
path: |
showbridge_*.tar.gz
- name: upload artifacts to release
uses: softprops/action-gh-release@v3
if: github.ref_type == 'tag' && github.event_name != 'workflow_dispatch' && startsWith(github.ref, 'refs/tags/v')
with:
draft: true
files: showbridge_*.tar.gz
windows:
runs-on: ubuntu-latest
needs: setup_release
env:
SHOWBRIDGE_VERSION: ${{ needs.setup_release.outputs.version }}
SHORT_COMMIT: ${{ needs.setup_release.outputs.short_commit }}
steps:
- name: checkout
uses: actions/checkout@v7
with:
fetch-depth: 0
- name: setup go
uses: actions/setup-go@v7
with:
go-version-file: "go.mod"
- name: setup zig
uses: mlugg/setup-zig@v2
with:
version: 0.16.0
- name: build amd64
run: |
CGO_ENABLED=1 GOOS=windows GOARCH=amd64 CC="zig cc -target x86_64-windows-gnu" CXX="zig c++ -target x86_64-windows-gnu" go build -o showbridge.exe -ldflags="-s -w -X main.version=${SHOWBRIDGE_VERSION}-${SHORT_COMMIT}" ./cmd/showbridge
zip showbridge_${SHOWBRIDGE_VERSION}_windows-amd64.zip showbridge.exe
- name: build arm64
run: |
CGO_ENABLED=1 GOOS=windows GOARCH=arm64 CC="zig cc -target aarch64-windows-gnu" CXX="zig c++ -target aarch64-windows-gnu" go build -o showbridge.exe -ldflags="-s -w -X main.version=${SHOWBRIDGE_VERSION}-${SHORT_COMMIT}" ./cmd/showbridge
zip showbridge_${SHOWBRIDGE_VERSION}_windows-arm64.zip showbridge.exe
- name: upload windows artifacts
uses: actions/upload-artifact@v7
if: github.event_name == 'workflow_dispatch'
with:
name: showbridge-windows
path: |
showbridge_*.zip
- name: upload artifacts to release
uses: softprops/action-gh-release@v3
if: github.ref_type == 'tag' && github.event_name != 'workflow_dispatch' && startsWith(github.ref, 'refs/tags/v')
with:
draft: true
files: showbridge_*.zip
macos_arm64:
runs-on: macos-26
needs: setup_release
env:
SHOWBRIDGE_VERSION: ${{ needs.setup_release.outputs.version }}
SHORT_COMMIT: ${{ needs.setup_release.outputs.short_commit }}
steps:
- name: checkout
uses: actions/checkout@v7
with:
fetch-depth: 0
- name: setup go
uses: actions/setup-go@v7
with:
go-version-file: "go.mod"
- name: build
run: |
CGO_ENABLED=1 GOOS=darwin GOARCH=arm64 go build -o showbridge -ldflags="-s -w -X main.version=${SHOWBRIDGE_VERSION}-${SHORT_COMMIT}" ./cmd/showbridge
tar -czf showbridge_${SHOWBRIDGE_VERSION}_macOS-arm64.tar.gz showbridge
- name: upload macos artifacts
uses: actions/upload-artifact@v7
if: github.event_name == 'workflow_dispatch'
with:
name: showbridge-macos-arm64
path: |
showbridge_*.tar.gz
- name: upload artifacts to release
uses: softprops/action-gh-release@v3
if: github.ref_type == 'tag' && github.event_name != 'workflow_dispatch' && startsWith(github.ref, 'refs/tags/v')
with:
draft: true
files: showbridge_*.tar.gz
macos_amd64:
runs-on: macos-26-intel
needs: setup_release
env:
SHOWBRIDGE_VERSION: ${{ needs.setup_release.outputs.version }}
SHORT_COMMIT: ${{ needs.setup_release.outputs.short_commit }}
steps:
- name: checkout
uses: actions/checkout@v7
with:
fetch-depth: 0
- name: setup go
uses: actions/setup-go@v7
with:
go-version-file: "go.mod"
- name: build
run: |
CGO_ENABLED=1 GOOS=darwin GOARCH=amd64 go build -o showbridge -ldflags="-s -w -X main.version=${SHOWBRIDGE_VERSION}-${SHORT_COMMIT}" ./cmd/showbridge
tar -czf showbridge_${SHOWBRIDGE_VERSION}_macOS-amd64.tar.gz showbridge
- name: upload macos artifacts
uses: actions/upload-artifact@v7
if: github.event_name == 'workflow_dispatch'
with:
name: showbridge-macos-amd64
path: |
showbridge_*.tar.gz
- name: upload artifacts to release
uses: softprops/action-gh-release@v3
if: github.ref_type == 'tag' && github.event_name != 'workflow_dispatch' && startsWith(github.ref, 'refs/tags/v')
with:
draft: true
files: showbridge_*.tar.gz
docker: docker:
runs-on: ubuntu-latest runs-on: ubuntu-latest
needs: setup_release needs: goreleaser
if: github.ref_type == 'tag' && github.event_name != 'workflow_dispatch'
steps: steps:
- name: Check out repository - name: Check out repository
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0
- name: Set up QEMU - name: Set up QEMU
uses: docker/setup-qemu-action@96fe6ef7f33517b61c61be40b68a1882f3264fb8 # v4.2.0 uses: docker/setup-qemu-action@ce360397dd3f832beb865e1373c09c0e9f86d70a # v4.0.0
- name: Set up Docker Buildx - name: Set up Docker Buildx
uses: docker/setup-buildx-action@bb05f3f5519dd87d3ba754cc423b652a5edd6d2c # v4.2.0 uses: docker/setup-buildx-action@4d04d5d9486b7bd6fa91e7baf45bbb4f8b9deedd # v4.0.0
- name: Login to Docker Hub - name: Login to Docker Hub
uses: docker/login-action@dbcb813823bdd20940b903addbd779551569679f # v4.6.0 uses: docker/login-action@b45d80f862d83dbcd57f89517bcf500b2ab88fb2 # v4.0.0
with: with:
username: ${{ secrets.DOCKERHUB_USERNAME }} username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }} password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Setup Docker metadata - name: Setup Docker metadata
id: meta id: meta
uses: docker/metadata-action@dc802804100637a589fabce1cb79ff13a1411302 # v6.2.0 uses: docker/metadata-action@030e881283bb7a6894de51c315a6bfe6a94e05cf # v6.0.0
with: with:
images: | images: |
jwetzell/showbridge jwetzell/showbridge
- name: Build and push - name: Build and push
uses: docker/build-push-action@53b7df96c91f9c12dcc8a07bcb9ccacbed38856a # v7.3.0 uses: docker/build-push-action@d08e5c354a6adb9ed34480a06d141179aa583294 # v7.0.0
with: with:
push: true push: true
context: ./ context: ./
+2 -2
View File
@@ -26,8 +26,8 @@ jobs:
- name: Install alsa - name: Install alsa
run: sudo apt-get install -y libasound2-dev run: sudo apt-get install -y libasound2-dev
- uses: actions/checkout@v7 - uses: actions/checkout@v6
- uses: actions/setup-go@v7 - uses: actions/setup-go@v6
with: with:
go-version-file: "go.mod" go-version-file: "go.mod"
- name: mod tidy - name: mod tidy
+2 -2
View File
@@ -1,6 +1,6 @@
ARG GO_VERSION=1.26.3 ARG GO_VERSION=1.26.0
FROM golang:${GO_VERSION}-alpine AS build FROM golang:${GO_VERSION}-alpine AS build
RUN apk --no-cache add ca-certificates tzdata git RUN apk --no-cache add ca-certificates tzdata
WORKDIR /build WORKDIR /build
COPY go.mod go.sum ./ COPY go.mod go.sum ./
RUN go mod download RUN go mod download
+1
View File
@@ -40,6 +40,7 @@ GLOBAL OPTIONS:
--config string path to config file (default: "./config.yaml") [$SHOWBRIDGE_CONFIG] --config string path to config file (default: "./config.yaml") [$SHOWBRIDGE_CONFIG]
--log-level string set log level (default: "info") [$SHOWBRIDGE_LOG_LEVEL] --log-level string set log level (default: "info") [$SHOWBRIDGE_LOG_LEVEL]
--log-format string log format to use (default: "text") [$SHOWBRIDGE_LOG_FORMAT] --log-format string log format to use (default: "text") [$SHOWBRIDGE_LOG_FORMAT]
--trace enable OpenTelemetry tracing [$SHOWBRIDGE_TRACE]
--help, -h show help --help, -h show help
--version, -v print the version --version, -v print the version
``` ```
+38 -61
View File
@@ -1,4 +1,4 @@
package api package showbridge
import ( import (
"context" "context"
@@ -6,84 +6,60 @@ import (
"encoding/json" "encoding/json"
"fmt" "fmt"
"io" "io"
"log/slog"
"net/http" "net/http"
"sync"
"time" "time"
"github.com/jwetzell/showbridge-go/internal/common"
"github.com/jwetzell/showbridge-go/internal/config" "github.com/jwetzell/showbridge-go/internal/config"
"github.com/jwetzell/showbridge-go/internal/module"
"github.com/jwetzell/showbridge-go/internal/route"
"github.com/jwetzell/showbridge-go/internal/schema" "github.com/jwetzell/showbridge-go/internal/schema"
) )
type ApiServer struct { func (r *Router) startAPIServer(config config.ApiConfig) {
config config.ApiConfig if !config.Enabled {
serverMu sync.Mutex r.logger.Warn("API not enabled")
server *http.Server
logger *slog.Logger
configurableRouter config.Configurable
eventRouter common.EventRouter
}
func NewApiServer(configurableRouter config.Configurable, eventRouter common.EventRouter) *ApiServer {
return &ApiServer{
configurableRouter: configurableRouter,
eventRouter: eventRouter,
logger: slog.Default().With("component", "api"),
}
}
func (as *ApiServer) Start(config config.ApiConfig) {
as.config = config
if !as.config.Enabled {
as.logger.Warn("not enabled")
return return
} }
as.logger.Debug("starting", "port", as.config.Port) r.logger.Debug("starting API server", "port", config.Port)
mux := http.NewServeMux() mux := http.NewServeMux()
mux.HandleFunc("/ws", as.handleWebsocket) mux.HandleFunc("/ws", r.handleWebsocket)
mux.HandleFunc("/health", as.handleHealthHTTP) mux.HandleFunc("/health", r.handleHealthHTTP)
mux.HandleFunc("/api/v1/config", as.handleConfigHTTP) mux.HandleFunc("/api/v1/config", r.handleConfigHTTP)
mux.HandleFunc("/schema/config.schema.json", handleConfigSchema) mux.HandleFunc("/schema/config.schema.json", handleConfigSchema)
mux.HandleFunc("/schema/routes.schema.json", handleRoutesSchema) mux.HandleFunc("/schema/routes.schema.json", handleRoutesSchema)
mux.HandleFunc("/schema/modules.schema.json", handleModulesSchema) mux.HandleFunc("/schema/modules.schema.json", handleModulesSchema)
mux.HandleFunc("/schema/processors.schema.json", handleProcessorsSchema) mux.HandleFunc("/schema/processors.schema.json", handleProcessorsSchema)
as.serverMu.Lock() r.apiServerMu.Lock()
defer as.serverMu.Unlock() defer r.apiServerMu.Unlock()
as.server = &http.Server{ r.apiServer = &http.Server{
Addr: fmt.Sprintf(":%d", as.config.Port), Addr: fmt.Sprintf(":%d", config.Port),
ReadHeaderTimeout: 5 * time.Second, Handler: mux,
Handler: mux,
} }
go func() { go func() {
err := as.server.ListenAndServe() r.apiServer.ListenAndServe()
if err != nil && err != http.ErrServerClosed { r.apiServerShutdown()
as.logger.Error("server error", "error", err)
}
}() }()
} }
func (as *ApiServer) Stop() { func (r *Router) stopAPIServer() {
if as.server == nil { if r.apiServer == nil {
return return
} }
as.serverMu.Lock() r.logger.Debug("stopping API server")
defer as.serverMu.Unlock() r.apiServerMu.Lock()
if as.server != nil { defer r.apiServerMu.Unlock()
if r.apiServer != nil {
apiShutdownCtx, apiShutdownCancel := context.WithTimeout(context.Background(), 5*time.Second) apiShutdownCtx, apiShutdownCancel := context.WithTimeout(context.Background(), 5*time.Second)
defer apiShutdownCancel() r.apiServerShutdown = apiShutdownCancel
err := as.server.Shutdown(apiShutdownCtx) r.apiServer.Shutdown(apiShutdownCtx)
if err != nil { <-apiShutdownCtx.Done()
as.logger.Error("error shutting down server", "error", err) r.apiServer = nil
}
as.server = nil
} }
as.logger.Debug("done")
} }
func (as *ApiServer) handleHealthHTTP(w http.ResponseWriter, req *http.Request) { func (r *Router) handleHealthHTTP(w http.ResponseWriter, req *http.Request) {
switch req.Method { switch req.Method {
case http.MethodGet: case http.MethodGet:
w.Header().Set("Access-Control-Allow-Origin", "*") w.Header().Set("Access-Control-Allow-Origin", "*")
@@ -99,11 +75,11 @@ func (as *ApiServer) handleHealthHTTP(w http.ResponseWriter, req *http.Request)
} }
} }
func (as *ApiServer) handleConfigHTTP(w http.ResponseWriter, req *http.Request) { func (r *Router) handleConfigHTTP(w http.ResponseWriter, req *http.Request) {
switch req.Method { switch req.Method {
case http.MethodGet: case http.MethodGet:
configJSON, err := json.Marshal(as.configurableRouter.GetRunningConfig()) configJSON, err := json.Marshal(r.runningConfig)
if err != nil { if err != nil {
http.Error(w, "Internal server error", http.StatusInternalServerError) http.Error(w, "Internal server error", http.StatusInternalServerError)
return return
@@ -113,6 +89,10 @@ func (as *ApiServer) handleConfigHTTP(w http.ResponseWriter, req *http.Request)
w.Header().Set("Content-Type", "application/json") w.Header().Set("Content-Type", "application/json")
w.Write(configJSON) w.Write(configJSON)
case http.MethodPut: case http.MethodPut:
if r.updatingConfig {
http.Error(w, "Config update in progress.", http.StatusConflict)
return
}
//TODO(jwetzell): again way too much marshaling //TODO(jwetzell): again way too much marshaling
cfgBytes, err := io.ReadAll(req.Body) cfgBytes, err := io.ReadAll(req.Body)
if err != nil { if err != nil {
@@ -152,15 +132,11 @@ func (as *ApiServer) handleConfigHTTP(w http.ResponseWriter, req *http.Request)
http.Error(w, "Bad request", http.StatusBadRequest) http.Error(w, "Bad request", http.StatusBadRequest)
return return
} }
moduleErrors, routeErrors, err := as.configurableRouter.UpdateConfig(newConfig, true) moduleErrors, routeErrors := r.UpdateConfig(newConfig)
if err != nil {
http.Error(w, err.Error(), http.StatusConflict)
return
}
if len(moduleErrors) > 0 || len(routeErrors) > 0 { if len(moduleErrors) > 0 || len(routeErrors) > 0 {
errorResponse := struct { errorResponse := struct {
ModuleErrors []config.ModuleError `json:"moduleErrors,omitempty"` ModuleErrors []module.ModuleError `json:"moduleErrors,omitempty"`
RouteErrors []config.RouteError `json:"routeErrors,omitempty"` RouteErrors []route.RouteError `json:"routeErrors,omitempty"`
}{ }{
ModuleErrors: moduleErrors, ModuleErrors: moduleErrors,
RouteErrors: routeErrors, RouteErrors: routeErrors,
@@ -178,6 +154,7 @@ func (as *ApiServer) handleConfigHTTP(w http.ResponseWriter, req *http.Request)
} }
w.Header().Set("Access-Control-Allow-Origin", "*") w.Header().Set("Access-Control-Allow-Origin", "*")
w.WriteHeader(http.StatusOK) w.WriteHeader(http.StatusOK)
r.ConfigChange <- newConfig
case http.MethodOptions: case http.MethodOptions:
w.Header().Set("Access-Control-Allow-Origin", "*") w.Header().Set("Access-Control-Allow-Origin", "*")
w.Header().Set("Access-Control-Allow-Methods", "GET, PUT, OPTIONS") w.Header().Set("Access-Control-Allow-Methods", "GET, PUT, OPTIONS")
+5
View File
@@ -0,0 +1,5 @@
package main
import "syscall/js"
var document = js.Global().Get("document")
+71
View File
@@ -0,0 +1,71 @@
<html>
<head>
<meta charset="utf-8" />
<script src="wasm_exec.js"></script>
<style>
body {
font-family: Arial, sans-serif;
display: flex;
flex-direction: column;
height: 100vh;
margin: 0;
padding: 0;
}
#output-container {
flex-grow: 1;
}
#log-container {
background-color: #1e1e1e;
width: 100%;
height: 200px;
overflow-y: auto;
overflow-x: scroll;
box-shadow: inset 0 2px 8px rgba(0, 0, 0, 0.5);
}
#logs {
margin: 0;
padding-left: 2px;
font-family: 'Consolas', 'Monaco', 'Courier New', monospace;
font-size: 13px;
line-height: 1.4;
color: #a8cc8c;
}
</style>
<script>
const container = document.getElementById("log-container");
function isScrolledToBottom() {
const scrollThreshold = 1;
return (
container.scrollHeight - container.clientHeight <=
container.scrollTop + scrollThreshold
);
}
function scrollToBottomManual() {
const container = document.getElementById("log-container");
// Set scrollTop to the maximum value possible (scrollHeight - clientHeight)
container.scrollTop = container.scrollHeight - container.clientHeight;
}
</script>
</head>
<body>
<div id="output-container">
<div id="output"></div>
</div>
<div id="log-container">
<pre id="logs"></pre>
</div>
<script>
const go = new Go();
WebAssembly.instantiateStreaming(
fetch("main.wasm"),
go.importObject,
).then((result) => {
go.run(result.instance);
});
</script>
</body>
</html>
+42
View File
@@ -0,0 +1,42 @@
package main
import "syscall/js"
type LogWriter struct {
Element js.Value
Container js.Value
}
func (pw *LogWriter) ScrollToBottom() {
scrollHeight := pw.Container.Get("scrollHeight").Int()
clientHeight := pw.Container.Get("clientHeight").Int()
pw.Container.Set("scrollTop", scrollHeight-clientHeight)
}
func (pw *LogWriter) IsScrolledToBottom() bool {
scrollHeight := pw.Container.Get("scrollHeight").Int()
clientHeight := pw.Container.Get("clientHeight").Int()
scrollTop := pw.Container.Get("scrollTop").Int()
return scrollHeight-clientHeight <= scrollTop+25
}
func (pw *LogWriter) Write(p []byte) (n int, err error) {
if !pw.Element.IsUndefined() {
currentText := pw.Element.Get("textContent").String()
newText := currentText + string(p)
pw.Element.Set("textContent", newText)
if pw.IsScrolledToBottom() {
pw.ScrollToBottom()
}
}
return len(p), nil
}
func NewLogWriter(id string) *LogWriter {
element := document.Call("getElementById", id)
container := element.Get("parentElement")
return &LogWriter{
Element: element,
Container: container,
}
}
+71
View File
@@ -0,0 +1,71 @@
package main
import (
"context"
"fmt"
"log/slog"
"github.com/jwetzell/showbridge-go"
"github.com/jwetzell/showbridge-go/internal/config"
)
func main() {
slog.SetLogLoggerLevel(slog.LevelDebug)
slog.SetDefault(slog.New(slog.NewTextHandler(NewLogWriter("logs"), &slog.HandlerOptions{
Level: slog.LevelDebug,
})))
router, moduleConfigErrors, routeConfigErrors := showbridge.NewRouter(config.Config{
Api: config.ApiConfig{
Enabled: false,
Port: 0,
},
Modules: []config.ModuleConfig{
{
Id: "midi",
Type: "midi.input",
Params: map[string]any{
"port": "Launchpad S",
},
},
},
Routes: []config.RouteConfig{
{
Input: "midi",
Processors: []config.ProcessorConfig{
{
Type: "debug.log",
},
{
Type: "web.set",
Params: map[string]any{
"id": "output",
"property": "textContent",
"value": "{{.Payload}}",
},
},
},
},
},
})
if len(moduleConfigErrors) > 0 {
for _, err := range moduleConfigErrors {
println("Module config error:", err.Error)
}
}
if len(routeConfigErrors) > 0 {
for _, err := range routeConfigErrors {
println("Route config error:", err.Error)
}
}
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
go func() {
router.Start(ctx)
fmt.Println("router stopped")
}()
<-ctx.Done()
}
+2
View File
@@ -0,0 +1,2 @@
# Added by goreleaser init:
dist/
+33
View File
@@ -0,0 +1,33 @@
version: 2
project_name: showbridge
before:
hooks:
- go mod tidy
builds:
- env:
- CGO_ENABLED=0
goos:
- linux
- windows
- darwin
goarch:
- "amd64"
- "arm64"
ldflags:
- "-s -w -X main.version={{.RawVersion}}-{{.ShortCommit}}"
archives:
- formats: [tar.gz]
name_template: "{{ .ProjectName }}_{{ .Version }}_{{ .Os }}-{{ .Arch }}"
format_overrides:
- goos: windows
formats: [zip]
changelog:
sort: asc
use: github-native
release:
draft: true
replace_existing_draft: true
+70 -23
View File
@@ -14,8 +14,15 @@ import (
"github.com/jwetzell/showbridge-go" "github.com/jwetzell/showbridge-go"
"github.com/jwetzell/showbridge-go/internal/config" "github.com/jwetzell/showbridge-go/internal/config"
"github.com/jwetzell/showbridge-go/internal/module"
"github.com/jwetzell/showbridge-go/internal/route"
"github.com/jwetzell/showbridge-go/internal/schema" "github.com/jwetzell/showbridge-go/internal/schema"
"github.com/urfave/cli/v3" "github.com/urfave/cli/v3"
"go.opentelemetry.io/otel"
"go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp"
"go.opentelemetry.io/otel/sdk/resource"
sdktrace "go.opentelemetry.io/otel/sdk/trace"
semconv "go.opentelemetry.io/otel/semconv/v1.40.0"
"sigs.k8s.io/yaml" "sigs.k8s.io/yaml"
) )
@@ -62,6 +69,12 @@ func main() {
}, },
Sources: cli.EnvVars("SHOWBRIDGE_LOG_FORMAT"), Sources: cli.EnvVars("SHOWBRIDGE_LOG_FORMAT"),
}, },
&cli.BoolFlag{
Name: "trace",
Value: false,
Usage: "enable OpenTelemetry tracing",
Sources: cli.EnvVars("SHOWBRIDGE_TRACE"),
},
}, },
Action: run, Action: run,
} }
@@ -79,11 +92,12 @@ func main() {
} }
type showbridgeApp struct { type showbridgeApp struct {
ctx context.Context ctx context.Context
configPath string configPath string
logger *slog.Logger logger *slog.Logger
router *showbridge.Router router *showbridge.Router
routerMutex sync.Mutex routerRunner *sync.WaitGroup
routerMutex sync.Mutex
} }
func readConfig(configPath string) (config.Config, error) { func readConfig(configPath string) (config.Config, error) {
@@ -106,18 +120,15 @@ func readConfig(configPath string) (config.Config, error) {
err = schema.ApplyDefaults(&yamlMap) err = schema.ApplyDefaults(&yamlMap)
if err != nil { if err != nil {
return config.Config{}, fmt.Errorf("failed to apply defaults: %w", err) return config.Config{}, err
} }
err = schema.ValidateConfig(yamlMap) err = schema.ValidateConfig(yamlMap)
if err != nil { if err != nil {
return config.Config{}, fmt.Errorf("failed to validate config: %w", err) return config.Config{}, err
} }
validatedConfigBytes, err := json.Marshal(yamlMap) validatedConfigBytes, err := json.Marshal(yamlMap)
if err != nil {
return config.Config{}, err
}
err = json.Unmarshal(validatedConfigBytes, &cfg) err = json.Unmarshal(validatedConfigBytes, &cfg)
if err != nil { if err != nil {
@@ -133,7 +144,7 @@ func writeConfig(configPath string, newConfig config.Config) error {
return err return err
} }
err = os.WriteFile(configPath, configBytes, 0600) err = os.WriteFile(configPath, configBytes, 0644)
if err != nil { if err != nil {
return err return err
} }
@@ -147,7 +158,7 @@ func run(ctx context.Context, c *cli.Command) error {
return errors.New("config path cannot be empty") return errors.New("config path cannot be empty")
} }
var logLevel slog.Level logLevel := slog.LevelInfo
logLevelFromFlag := c.String("log-level") logLevelFromFlag := c.String("log-level")
@@ -185,10 +196,24 @@ func run(ctx context.Context, c *cli.Command) error {
slog.SetDefault(slog.New(logHandler)) slog.SetDefault(slog.New(logHandler))
if c.Bool("trace") {
exporter, err := otlptracehttp.New(ctx)
if err != nil {
return fmt.Errorf("failed to create trace exporter: %w", err)
}
tracerProvider := newTracerProvider(exporter)
otel.SetTracerProvider(tracerProvider)
defer tracerProvider.Shutdown(ctx)
otel.SetTracerProvider(tracerProvider)
}
showbridgeApp := &showbridgeApp{ showbridgeApp := &showbridgeApp{
ctx: ctx, ctx: ctx,
configPath: configPath, configPath: configPath,
logger: slog.Default().With("component", "cmd"), logger: slog.Default().With("component", "cmd"),
routerRunner: &sync.WaitGroup{},
} }
config, err := readConfig(showbridgeApp.configPath) config, err := readConfig(showbridgeApp.configPath)
@@ -204,10 +229,15 @@ func run(ctx context.Context, c *cli.Command) error {
return fmt.Errorf("errors initializing modules or routes") return fmt.Errorf("errors initializing modules or routes")
} }
if err != nil {
return fmt.Errorf("failed to initialize router: %w", err)
}
showbridgeApp.routerMutex.Lock() showbridgeApp.routerMutex.Lock()
showbridgeApp.router = router showbridgeApp.router = router
router.Start(context.Background()) showbridgeApp.routerRunner.Go(func() {
router.Start(context.Background())
})
showbridgeApp.routerMutex.Unlock() showbridgeApp.routerMutex.Unlock()
go showbridgeApp.handleChannels() go showbridgeApp.handleChannels()
@@ -215,6 +245,8 @@ func run(ctx context.Context, c *cli.Command) error {
<-showbridgeApp.ctx.Done() <-showbridgeApp.ctx.Done()
showbridgeApp.logger.Debug("shutting down router") showbridgeApp.logger.Debug("shutting down router")
showbridgeApp.router.Stop() showbridgeApp.router.Stop()
showbridgeApp.logger.Debug("waiting for router to exit")
showbridgeApp.routerRunner.Wait()
return nil return nil
} }
@@ -230,12 +262,7 @@ func (app *showbridgeApp) handleChannels() {
app.routerMutex.Unlock() app.routerMutex.Unlock()
continue continue
} }
moduleErrors, routeErrors, err := app.router.UpdateConfig(config, false) moduleErrors, routeErrors := app.router.UpdateConfig(config)
if err != nil {
app.logger.Error("failed to update router config", "error", err)
app.routerMutex.Unlock()
continue
}
app.logConfigErrors(moduleErrors, routeErrors) app.logConfigErrors(moduleErrors, routeErrors)
app.logger.Info("configuration reloaded successfully") app.logger.Info("configuration reloaded successfully")
app.routerMutex.Unlock() app.routerMutex.Unlock()
@@ -253,7 +280,7 @@ func (app *showbridgeApp) handleChannels() {
} }
} }
func (app *showbridgeApp) logConfigErrors(moduleErrors []config.ModuleError, routeErrors []config.RouteError) { func (app *showbridgeApp) logConfigErrors(moduleErrors []module.ModuleError, routeErrors []route.RouteError) {
for _, moduleError := range moduleErrors { for _, moduleError := range moduleErrors {
app.logger.Error("problem initializing module", "index", moduleError.Index, "error", moduleError.Error) app.logger.Error("problem initializing module", "index", moduleError.Index, "error", moduleError.Error)
} }
@@ -262,3 +289,23 @@ func (app *showbridgeApp) logConfigErrors(moduleErrors []config.ModuleError, rou
app.logger.Error("problem initializing route", "index", routeError.Index, "error", routeError.Error) app.logger.Error("problem initializing route", "index", routeError.Index, "error", routeError.Error)
} }
} }
func newTracerProvider(exp sdktrace.SpanExporter) *sdktrace.TracerProvider {
r, err := resource.Merge(
resource.Default(),
resource.NewWithAttributes(
semconv.SchemaURL,
semconv.ServiceName("showbridge"),
semconv.ServiceVersion(version),
),
)
if err != nil {
panic(err)
}
return sdktrace.NewTracerProvider(
sdktrace.WithBatcher(exp),
sdktrace.WithResource(r),
)
}
-85
View File
@@ -1,85 +0,0 @@
package showbridge
import (
"errors"
"reflect"
"github.com/jwetzell/showbridge-go/internal/common"
"github.com/jwetzell/showbridge-go/internal/config"
"github.com/jwetzell/showbridge-go/internal/route"
)
func (r *Router) GetRunningConfig() config.Config {
r.runningConfigMu.RLock()
defer r.runningConfigMu.RUnlock()
return r.runningConfig
}
func (r *Router) UpdateConfig(newConfig config.Config, triggerChangeChan bool) ([]config.ModuleError, []config.RouteError, error) {
if !r.runningConfigMu.TryLock() {
return nil, nil, errors.New("config update in progress")
}
defer r.runningConfigMu.Unlock()
oldConfig := r.runningConfig
r.logger.Debug("received config update", "oldConfig", oldConfig, "newConfig", newConfig)
if !reflect.DeepEqual(oldConfig.Api, newConfig.Api) {
r.logger.Info("applying new API config")
r.apiServer.Stop()
r.apiServer.Start(newConfig.Api)
r.runningConfig.Api = newConfig.Api
}
// TODO(jwetzell): handle config update errors better
for _, moduleInstance := range r.ModuleInstances {
moduleInstance.Stop()
}
r.logger.Debug("waiting for modules to exit")
r.moduleWait.Wait()
r.ModuleInstances = make(map[string]common.Module)
r.RouteInstances = []*route.Route{}
var moduleErrors []config.ModuleError
for moduleIndex, moduleDecl := range newConfig.Modules {
err := r.addModule(moduleDecl)
if err != nil {
if moduleErrors == nil {
moduleErrors = []config.ModuleError{}
}
moduleErrors = append(moduleErrors, config.ModuleError{
Index: moduleIndex,
Config: moduleDecl,
Error: err.Error(),
})
continue
}
}
var routeErrors []config.RouteError
for routeIndex, routeDecl := range newConfig.Routes {
err := r.addRoute(routeDecl)
if err != nil {
if routeErrors == nil {
routeErrors = []config.RouteError{}
}
routeErrors = append(routeErrors, config.RouteError{
Index: routeIndex,
Config: routeDecl,
Error: err.Error(),
})
continue
}
}
r.runningConfig = newConfig
r.startModules()
if triggerChangeChan {
r.ConfigChange <- newConfig
}
return moduleErrors, routeErrors, nil
}
-135
View File
@@ -1,135 +0,0 @@
api:
enabled: true
port: 8080
modules:
- id: sqlite
type: db.sqlite
params:
dsn: ":memory:"
- id: httpin
type: http.server
params:
port: 3000
- id: tcpout
type: net.tcp.client
params:
host: "localhost"
port: 9000
framing: "RAW"
- id: tcpin
type: net.tcp.server
params:
port: 9000
framing: "RAW"
- id: ticker
type: time.interval
params:
duration: 1000
- id: udpout
type: net.udp.client
params:
host: "localhost"
port: 9001
- id: udpin
type: net.udp.server
params:
port: 9001
- id: wsecho
type: websocket.client
params:
url: "wss://echo.websocket.org"
- id: natsserver
type: nats.server
params:
port: 4222
- id: natsout
type: nats.client
params:
url: "nats://localhost:4222"
subject: "hello"
- id: natsin
type: nats.client
params:
url: "nats://localhost:4222"
subject: "test"
routes:
- input: ticker
processors:
- type: string.create
params:
template: "wsecho: {{.Payload.UnixMilli}}"
- type: module.output
params:
module: wsecho
- input: ticker
processors:
- type: string.create
params:
template: "tcpout: {{.Payload.UnixMilli}}"
- type: string.encode
- type: module.output
params:
module: tcpout
- input: ticker
processors:
- type: string.create
params:
template: "udpout: {{.Payload.UnixMilli}}"
- type: string.encode
- type: module.output
params:
module: udpout
- input: ticker
processors:
- type: http.request.do
params:
url: "http://localhost:3000/echo"
method: "GET"
- type: struct.field.get
params:
name: "Body"
- type: string.decode
- type: debug.log
- input: tcpin
processors:
- type: string.decode
- type: debug.log
- input: udpin
processors:
- type: string.decode
- type: debug.log
- input: wsecho
processors:
- type: debug.log
- input: ticker
processors:
- type: db.query
params:
module: "sqlite"
query: "SELECT 1;"
- type: debug.log
- input: natsin
processors:
- type: struct.field.get
params:
name: "Data"
- type: string.decode
- type: debug.log
- input: httpin
processors:
- type: http.response.create
params:
status: 200
bodyTemplate: "Hello, World!"
- type: module.output
params:
module: httpin
- input: ticker
processors:
- type: string.create
params:
template: "natsout: {{.Payload}}"
- type: pubsub.publish
params:
module: natsout
topic: "test"
+3 -7
View File
@@ -13,15 +13,11 @@ modules:
port: 8000 port: 8000
routes: routes:
- input: http - input: http
id: http-to-osc
processors: processors:
- id: create-osc - type: osc.message.create
type: osc.message.create
params: params:
address: "{{.Payload.URL.Path}}" address: "{{.Payload.URL.Path}}"
- id: encode-osc - type: osc.message.encode
type: osc.message.encode - type: router.output
- id: output-osc
type: module.output
params: params:
module: udp module: udp
+43 -32
View File
@@ -1,53 +1,64 @@
package showbridge package showbridge
import ( import (
"time" "encoding/json"
"github.com/jwetzell/showbridge-go/internal/common" "github.com/gorilla/websocket"
) )
func (r *Router) HandleEvent(event common.Event, sender common.EventDestination) { type Event struct {
Type string `json:"type"`
Data any `json:"data,omitempty"`
Error string `json:"error,omitempty"`
}
func (e Event) toJSON() ([]byte, error) {
return json.Marshal(e)
}
func (r *Router) handleEvent(event Event, sender *websocket.Conn) {
switch event.Type { switch event.Type {
case "ping": case "ping":
r.unicastEvent(common.Event{Type: "pong", Data: map[string]any{ r.unicastEvent(Event{Type: "pong"}, sender)
"timestamp": time.Now().UnixMilli(),
}}, sender)
default: default:
r.logger.Warn("unknown event type", "eventType", event.Type) r.logger.Warn("unknown event type", "eventType", event.Type)
} }
} }
func (r *Router) AddEventDestination(dest common.EventDestination) { func (r *Router) unicastEvent(event Event, conn *websocket.Conn) {
r.eventDestinationsMu.Lock() eventJSON, err := event.toJSON()
defer r.eventDestinationsMu.Unlock()
r.eventDestinations = append(r.eventDestinations, dest)
}
func (r *Router) RemoveEventDestination(dest common.EventDestination) {
r.eventDestinationsMu.Lock()
defer r.eventDestinationsMu.Unlock()
for i, d := range r.eventDestinations {
if d.Is(dest) {
r.eventDestinations = append(r.eventDestinations[:i], r.eventDestinations[i+1:]...)
break
}
}
}
func (r *Router) unicastEvent(event common.Event, dest common.EventDestination) {
err := dest.Send(event)
if err != nil { if err != nil {
r.logger.Error("failed to send event", "error", err) r.logger.Error("failed to marshal event to JSON", "error", err)
return
}
err = conn.WriteMessage(websocket.TextMessage, eventJSON)
if err != nil {
r.logger.Error("failed to write message to websocket connection", "error", err)
} }
} }
func (r *Router) broadcastEvent(event common.Event) { func (r *Router) broadcastEvent(event Event, excluded ...*websocket.Conn) {
r.eventDestinationsMu.Lock() eventJSON, err := event.toJSON()
defer r.eventDestinationsMu.Unlock() if err != nil {
for _, dest := range r.eventDestinations { r.logger.Error("failed to marshal event to JSON", "error", err)
err := dest.Send(event) return
}
r.wsConnsMu.Lock()
defer r.wsConnsMu.Unlock()
for _, conn := range r.wsConns {
exclude := false
for _, excludedConn := range excluded {
if conn == excludedConn {
exclude = true
break
}
}
if exclude {
continue
}
err := conn.WriteMessage(websocket.TextMessage, eventJSON)
if err != nil { if err != nil {
r.logger.Error("failed to send event", "error", err) r.logger.Error("failed to write message to websocket connection", "error", err)
} }
} }
} }
+52 -36
View File
@@ -1,76 +1,92 @@
module github.com/jwetzell/showbridge-go module github.com/jwetzell/showbridge-go
go 1.26.3 go 1.26.0
require ( require (
github.com/eclipse/paho.mqtt.golang v1.5.1 github.com/eclipse/paho.mqtt.golang v1.5.1
github.com/emiago/diago v0.31.0 github.com/emiago/diago v0.28.0
github.com/emiago/sipgo v1.4.3 github.com/emiago/sipgo v1.2.1
github.com/expr-lang/expr v1.17.8 github.com/expr-lang/expr v1.17.8
github.com/extism/go-sdk v1.7.1 github.com/extism/go-sdk v1.7.1
github.com/google/jsonschema-go v0.4.3 github.com/google/jsonschema-go v0.4.2
github.com/gorilla/websocket v1.5.3 github.com/gorilla/websocket v1.5.3
github.com/jwetzell/artnet-go v0.3.0 github.com/jwetzell/artnet-go v0.2.1
github.com/jwetzell/free-d-go v0.2.0 github.com/jwetzell/free-d-go v0.1.0
github.com/jwetzell/osc-go v0.3.0 github.com/jwetzell/osc-go v0.2.0
github.com/jwetzell/psn-go v0.3.0 github.com/jwetzell/psn-go v0.3.0
github.com/nats-io/nats-server/v2 v2.14.4 github.com/nats-io/nats-server/v2 v2.12.6
github.com/nats-io/nats.go v1.52.0 github.com/nats-io/nats.go v1.49.0
github.com/redis/go-redis/v9 v9.22.0 github.com/redis/go-redis/v9 v9.18.0
github.com/urfave/cli/v3 v3.10.1 github.com/urfave/cli/v3 v3.7.0
gitlab.com/gomidi/midi/v2 v2.3.24 gitlab.com/gomidi/midi/v2 v2.3.23
go.bug.st/serial v1.8.0 go.bug.st/serial v1.6.4
golang.org/x/time v0.15.0 go.opentelemetry.io/otel v1.42.0
modernc.org/quickjs v0.23.0 go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.42.0
modernc.org/sqlite v1.56.0 go.opentelemetry.io/otel/sdk v1.42.0
go.opentelemetry.io/otel/trace v1.42.0
modernc.org/quickjs v0.17.1
modernc.org/sqlite v1.47.0
sigs.k8s.io/yaml v1.6.0 sigs.k8s.io/yaml v1.6.0
) )
require ( require (
github.com/antithesishq/antithesis-sdk-go v0.7.2-default-no-op // indirect github.com/antithesishq/antithesis-sdk-go v0.6.0-default-no-op // indirect
github.com/cenkalti/backoff/v5 v5.0.3 // indirect
github.com/cespare/xxhash/v2 v2.3.0 // indirect github.com/cespare/xxhash/v2 v2.3.0 // indirect
github.com/creack/goselect v0.1.2 // indirect
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect
github.com/dustin/go-humanize v1.0.1 // indirect github.com/dustin/go-humanize v1.0.1 // indirect
github.com/dylibso/observe-sdk/go v0.0.0-20240828172851-9145d8ad07e1 // indirect github.com/dylibso/observe-sdk/go v0.0.0-20240819160327-2d926c5d788a // indirect
github.com/emiago/dtls/v3 v3.0.0-20260122183559-8b8d23e359c0 // indirect github.com/emiago/dtls/v3 v3.0.0-20260122183559-8b8d23e359c0 // indirect
github.com/go-audio/riff v1.0.0 // indirect github.com/go-audio/riff v1.0.0 // indirect
github.com/go-logr/logr v1.4.3 // indirect
github.com/go-logr/stdr v1.2.2 // indirect
github.com/gobwas/glob v0.2.3 // indirect github.com/gobwas/glob v0.2.3 // indirect
github.com/gobwas/httphead v0.1.0 // indirect github.com/gobwas/httphead v0.1.0 // indirect
github.com/gobwas/pool v0.2.1 // indirect github.com/gobwas/pool v0.2.1 // indirect
github.com/gobwas/ws v1.4.0 // indirect github.com/gobwas/ws v1.4.0 // indirect
github.com/google/go-tpm v0.9.8 // indirect github.com/google/go-tpm v0.9.8 // indirect
github.com/google/uuid v1.6.0 // indirect github.com/google/uuid v1.6.0 // indirect
github.com/ianlancetaylor/demangle v0.0.0-20260505044615-1ff4bf46051f // indirect github.com/grpc-ecosystem/grpc-gateway/v2 v2.28.0 // indirect
github.com/ianlancetaylor/demangle v0.0.0-20240805132620-81f5be970eca // indirect
github.com/icholy/digest v1.1.0 // indirect github.com/icholy/digest v1.1.0 // indirect
github.com/klauspost/compress v1.19.0 // indirect github.com/klauspost/compress v1.18.4 // indirect
github.com/mattn/go-isatty v0.0.24 // indirect github.com/mattn/go-isatty v0.0.20 // indirect
github.com/minio/highwayhash v1.0.4 // indirect github.com/minio/highwayhash v1.0.4-0.20251030100505-070ab1a87a76 // indirect
github.com/nats-io/jwt/v2 v2.8.2 // indirect github.com/nats-io/jwt/v2 v2.8.1 // indirect
github.com/nats-io/nkeys v0.4.16 // indirect github.com/nats-io/nkeys v0.4.15 // indirect
github.com/nats-io/nuid v1.0.1 // indirect github.com/nats-io/nuid v1.0.1 // indirect
github.com/ncruces/go-strftime v1.0.0 // indirect github.com/ncruces/go-strftime v1.0.0 // indirect
github.com/pion/logging v0.2.4 // indirect github.com/pion/logging v0.2.4 // indirect
github.com/pion/randutil v0.1.0 // indirect github.com/pion/randutil v0.1.0 // indirect
github.com/pion/rtcp v1.2.16 // indirect github.com/pion/rtcp v1.2.16 // indirect
github.com/pion/rtp v1.10.2 // indirect github.com/pion/rtp v1.8.26 // indirect
github.com/pion/srtp/v3 v3.0.10 // indirect github.com/pion/srtp/v3 v3.0.9 // indirect
github.com/pion/transport/v3 v3.1.1 // indirect
github.com/pion/transport/v4 v4.0.1 // indirect github.com/pion/transport/v4 v4.0.1 // indirect
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
github.com/rogpeppe/go-internal v1.14.1 // indirect
github.com/tetratelabs/wabin v0.0.0-20230304001439-f6f874872834 // indirect github.com/tetratelabs/wabin v0.0.0-20230304001439-f6f874872834 // indirect
github.com/tetratelabs/wazero v1.11.0 // indirect github.com/tetratelabs/wazero v1.9.0 // indirect
github.com/zaf/g711 v1.4.0 // indirect github.com/zaf/g711 v1.4.0 // indirect
go.opentelemetry.io/proto/otlp v1.10.0 // indirect go.opentelemetry.io/auto/sdk v1.2.1 // indirect
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.42.0 // indirect
go.opentelemetry.io/otel/metric v1.42.0 // indirect
go.opentelemetry.io/proto/otlp v1.9.0 // indirect
go.uber.org/atomic v1.11.0 // indirect go.uber.org/atomic v1.11.0 // indirect
go.yaml.in/yaml/v2 v2.4.2 // indirect go.yaml.in/yaml/v2 v2.4.2 // indirect
go.yaml.in/yaml/v3 v3.0.4 // indirect golang.org/x/crypto v0.49.0 // indirect
golang.org/x/crypto v0.54.0 // indirect golang.org/x/net v0.51.0 // indirect
golang.org/x/net v0.56.0 // indirect golang.org/x/sync v0.20.0 // indirect
golang.org/x/sync v0.21.0 // indirect golang.org/x/sys v0.42.0 // indirect
golang.org/x/sys v0.47.0 // indirect golang.org/x/text v0.35.0 // indirect
golang.org/x/time v0.15.0 // indirect
google.golang.org/genproto/googleapis/api v0.0.0-20260209200024-4cfbd4190f57 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20260209200024-4cfbd4190f57 // indirect
google.golang.org/grpc v1.79.2 // indirect
google.golang.org/protobuf v1.36.11 // indirect google.golang.org/protobuf v1.36.11 // indirect
gopkg.in/hraban/opus.v2 v2.0.0-20230925203106-0188a62cb302 // indirect gopkg.in/hraban/opus.v2 v2.0.0-20230925203106-0188a62cb302 // indirect
modernc.org/libc v1.74.4 // indirect modernc.org/libc v1.70.0 // indirect
modernc.org/libquickjs v0.13.1 // indirect modernc.org/libquickjs v0.12.3 // indirect
modernc.org/mathutil v1.7.1 // indirect modernc.org/mathutil v1.7.1 // indirect
modernc.org/memory v1.11.0 // indirect modernc.org/memory v1.11.0 // indirect
) )
+127 -82
View File
@@ -1,31 +1,42 @@
github.com/antithesishq/antithesis-sdk-go v0.7.2-default-no-op h1:p2zFsAzvhIpFya8AIOHIbWf7NGvO34QpLGclyf7nXj8= github.com/antithesishq/antithesis-sdk-go v0.6.0-default-no-op h1:kpBdlEPbRvff0mDD1gk7o9BhI16b9p5yYAXRlidpqJE=
github.com/antithesishq/antithesis-sdk-go v0.7.2-default-no-op/go.mod h1:FQyySiasQQM8735Ddel3MRojmy4dA1IqCeyJ5jmPMbI= github.com/antithesishq/antithesis-sdk-go v0.6.0-default-no-op/go.mod h1:IUpT2DPAKh6i/YhSbt6Gl3v2yvUZjmKncl7U91fup7E=
github.com/bsm/ginkgo/v2 v2.12.0 h1:Ny8MWAHyOepLGlLKYmXG4IEkioBysk6GpaRTLC8zwWs= github.com/bsm/ginkgo/v2 v2.12.0 h1:Ny8MWAHyOepLGlLKYmXG4IEkioBysk6GpaRTLC8zwWs=
github.com/bsm/ginkgo/v2 v2.12.0/go.mod h1:SwYbGRRDovPVboqFv0tPTcG1sN61LM1Z4ARdbAV9g4c= github.com/bsm/ginkgo/v2 v2.12.0/go.mod h1:SwYbGRRDovPVboqFv0tPTcG1sN61LM1Z4ARdbAV9g4c=
github.com/bsm/gomega v1.27.10 h1:yeMWxP2pV2fG3FgAODIY8EiRE3dy0aeFYt4l7wh6yKA= github.com/bsm/gomega v1.27.10 h1:yeMWxP2pV2fG3FgAODIY8EiRE3dy0aeFYt4l7wh6yKA=
github.com/bsm/gomega v1.27.10/go.mod h1:JyEr/xRbxbtgWNi8tIEVPUYZ5Dzef52k01W3YH0H+O0= github.com/bsm/gomega v1.27.10/go.mod h1:JyEr/xRbxbtgWNi8tIEVPUYZ5Dzef52k01W3YH0H+O0=
github.com/cenkalti/backoff/v5 v5.0.3 h1:ZN+IMa753KfX5hd8vVaMixjnqRZ3y8CuJKRKj1xcsSM=
github.com/cenkalti/backoff/v5 v5.0.3/go.mod h1:rkhZdG3JZukswDf7f0cwqPNk4K0sa+F97BxZthm/crw=
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/creack/goselect v0.1.2 h1:2DNy14+JPjRBgPzAd1thbQp4BSIihxcBf0IXhQXDRa0=
github.com/creack/goselect v0.1.2/go.mod h1:a/NhLweNvqIYMuxcMOuWY516Cimucms3DglDzQP3hKY=
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/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78=
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc=
github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY= github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto= github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
github.com/dylibso/observe-sdk/go v0.0.0-20240828172851-9145d8ad07e1 h1:idfl8M8rPW93NehFw5H1qqH8yG158t5POr+LX9avbJY= github.com/dylibso/observe-sdk/go v0.0.0-20240819160327-2d926c5d788a h1:UwSIFv5g5lIvbGgtf3tVwC7Ky9rmMFBp0RMs+6f6YqE=
github.com/dylibso/observe-sdk/go v0.0.0-20240828172851-9145d8ad07e1/go.mod h1:C8DzXehI4zAbrdlbtOByKX6pfivJTBiV9Jjqv56Yd9Q= github.com/dylibso/observe-sdk/go v0.0.0-20240819160327-2d926c5d788a/go.mod h1:C8DzXehI4zAbrdlbtOByKX6pfivJTBiV9Jjqv56Yd9Q=
github.com/eclipse/paho.mqtt.golang v1.5.1 h1:/VSOv3oDLlpqR2Epjn1Q7b2bSTplJIeV2ISgCl2W7nE= github.com/eclipse/paho.mqtt.golang v1.5.1 h1:/VSOv3oDLlpqR2Epjn1Q7b2bSTplJIeV2ISgCl2W7nE=
github.com/eclipse/paho.mqtt.golang v1.5.1/go.mod h1:1/yJCneuyOoCOzKSsOTUc0AJfpsItBGWvYpBLimhArU= github.com/eclipse/paho.mqtt.golang v1.5.1/go.mod h1:1/yJCneuyOoCOzKSsOTUc0AJfpsItBGWvYpBLimhArU=
github.com/emiago/diago v0.31.0 h1:w/GZQnMS60EBANKXMnf/MjFDlUfcfHLPOlEKiZkaNe0= github.com/emiago/diago v0.28.0 h1:VCiimhFYLoBTsxH6WrufLSt6tKWG8Fv7LfCkZHqct2E=
github.com/emiago/diago v0.31.0/go.mod h1:CP7p9/aBZG/fQEYdtGdQkCblr1jLr1zbxODcu8MryVU= github.com/emiago/diago v0.28.0/go.mod h1:eQT6j9co9PMQ/25aUiM2jpvwxxWFXLWi2w5R3lZNmKg=
github.com/emiago/dtls/v3 v3.0.0-20260122183559-8b8d23e359c0 h1:o4LxpUnZ1zxiQ+Qjc9kLwXcjz31NGAHmnZ7xoJto3VM= github.com/emiago/dtls/v3 v3.0.0-20260122183559-8b8d23e359c0 h1:o4LxpUnZ1zxiQ+Qjc9kLwXcjz31NGAHmnZ7xoJto3VM=
github.com/emiago/dtls/v3 v3.0.0-20260122183559-8b8d23e359c0/go.mod h1:ydcZ977eS1I6uOWodzMuw30BwvNAzT9su/xcNYSJqjA= github.com/emiago/dtls/v3 v3.0.0-20260122183559-8b8d23e359c0/go.mod h1:ydcZ977eS1I6uOWodzMuw30BwvNAzT9su/xcNYSJqjA=
github.com/emiago/sipgo v1.4.3 h1:Ju1Ilp0LhTcSHCyp2jBYAQDQ8TAwuxQHzT1IhO6jSIY= github.com/emiago/sipgo v1.2.1 h1:5JTwogbe3yQFA3sjBVueN2Q4WTU350tGeBwPYT8HMv0=
github.com/emiago/sipgo v1.4.3/go.mod h1:DuwAxBZhKMqIzQFPGZb1MVAGU6Wuxj64oTOhd5dx/FY= github.com/emiago/sipgo v1.2.1/go.mod h1:DuwAxBZhKMqIzQFPGZb1MVAGU6Wuxj64oTOhd5dx/FY=
github.com/expr-lang/expr v1.17.8 h1:W1loDTT+0PQf5YteHSTpju2qfUfNoBt4yw9+wOEU9VM= github.com/expr-lang/expr v1.17.8 h1:W1loDTT+0PQf5YteHSTpju2qfUfNoBt4yw9+wOEU9VM=
github.com/expr-lang/expr v1.17.8/go.mod h1:8/vRC7+7HBzESEqt5kKpYXxrxkr31SaO8r40VO/1IT4= github.com/expr-lang/expr v1.17.8/go.mod h1:8/vRC7+7HBzESEqt5kKpYXxrxkr31SaO8r40VO/1IT4=
github.com/extism/go-sdk v1.7.1 h1:lWJos6uY+tRFdlIHR+SJjwFDApY7OypS/2nMhiVQ9Sw= github.com/extism/go-sdk v1.7.1 h1:lWJos6uY+tRFdlIHR+SJjwFDApY7OypS/2nMhiVQ9Sw=
github.com/extism/go-sdk v1.7.1/go.mod h1:IT+Xdg5AZM9hVtpFUA+uZCJMge/hbvshl8bwzLtFyKA= github.com/extism/go-sdk v1.7.1/go.mod h1:IT+Xdg5AZM9hVtpFUA+uZCJMge/hbvshl8bwzLtFyKA=
github.com/go-audio/riff v1.0.0 h1:d8iCGbDvox9BfLagY94fBynxSPHO80LmZCaOsmKxokA= github.com/go-audio/riff v1.0.0 h1:d8iCGbDvox9BfLagY94fBynxSPHO80LmZCaOsmKxokA=
github.com/go-audio/riff v1.0.0/go.mod h1:l3cQwc85y79NQFCRB7TiPoNiaijp6q8Z0Uv38rVG498= github.com/go-audio/riff v1.0.0/go.mod h1:l3cQwc85y79NQFCRB7TiPoNiaijp6q8Z0Uv38rVG498=
github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI=
github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
github.com/gobwas/glob v0.2.3 h1:A4xDbljILXROh+kObIiy5kIaPYD8e96x1tgBhUI5J+Y= github.com/gobwas/glob v0.2.3 h1:A4xDbljILXROh+kObIiy5kIaPYD8e96x1tgBhUI5J+Y=
github.com/gobwas/glob v0.2.3/go.mod h1:d3Ez4x06l9bZtSvzIay5+Yzi0fmZzPgnTbPcKjJAkT8= github.com/gobwas/glob v0.2.3/go.mod h1:d3Ez4x06l9bZtSvzIay5+Yzi0fmZzPgnTbPcKjJAkT8=
github.com/gobwas/httphead v0.1.0 h1:exrUm0f4YX0L7EBwZHuCF4GDp8aJfVeBrlLQrs6NqWU= github.com/gobwas/httphead v0.1.0 h1:exrUm0f4YX0L7EBwZHuCF4GDp8aJfVeBrlLQrs6NqWU=
@@ -34,52 +45,56 @@ github.com/gobwas/pool v0.2.1 h1:xfeeEhW7pwmX8nuLVlqbzVc7udMDrwetjEv+TZIz1og=
github.com/gobwas/pool v0.2.1/go.mod h1:q8bcK0KcYlCgd9e7WYLm9LpyS+YeLd8JVDW6WezmKEw= github.com/gobwas/pool v0.2.1/go.mod h1:q8bcK0KcYlCgd9e7WYLm9LpyS+YeLd8JVDW6WezmKEw=
github.com/gobwas/ws v1.4.0 h1:CTaoG1tojrh4ucGPcoJFiAQUAsEWekEWvLy7GsVNqGs= github.com/gobwas/ws v1.4.0 h1:CTaoG1tojrh4ucGPcoJFiAQUAsEWekEWvLy7GsVNqGs=
github.com/gobwas/ws v1.4.0/go.mod h1:G3gNqMNtPppf5XUz7O4shetPpcZ1VJ7zt18dlUeakrc= github.com/gobwas/ws v1.4.0/go.mod h1:G3gNqMNtPppf5XUz7O4shetPpcZ1VJ7zt18dlUeakrc=
github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek=
github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps=
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
github.com/google/go-tpm v0.9.8 h1:slArAR9Ft+1ybZu0lBwpSmpwhRXaa85hWtMinMyRAWo= github.com/google/go-tpm v0.9.8 h1:slArAR9Ft+1ybZu0lBwpSmpwhRXaa85hWtMinMyRAWo=
github.com/google/go-tpm v0.9.8/go.mod h1:h9jEsEECg7gtLis0upRBQU+GhYVH6jMjrFxI8u6bVUY= github.com/google/go-tpm v0.9.8/go.mod h1:h9jEsEECg7gtLis0upRBQU+GhYVH6jMjrFxI8u6bVUY=
github.com/google/jsonschema-go v0.4.3 h1:/DBOLZTfDow7pe2GmaJNhltueGTtDKICi8V8p+DQPd0= github.com/google/jsonschema-go v0.4.2 h1:tmrUohrwoLZZS/P3x7ex0WAVknEkBZM46iALbcqoRA8=
github.com/google/jsonschema-go v0.4.3/go.mod h1:r5quNTdLOYEz95Ru18zA0ydNbBuYoo9tgaYcxEYhJVE= github.com/google/jsonschema-go v0.4.2/go.mod h1:r5quNTdLOYEz95Ru18zA0ydNbBuYoo9tgaYcxEYhJVE=
github.com/google/pprof v0.0.0-20260802141513-ef3492d7dac3 h1:LMLX+LgTNWpfvCBdFebv6EsYotImrt/Ppc5cXIriCSo= github.com/google/pprof v0.0.0-20250317173921-a4b03ec1a45e h1:ijClszYn+mADRFY17kjQEVQ1XRhq2/JR1M3sGqeJoxs=
github.com/google/pprof v0.0.0-20260802141513-ef3492d7dac3/go.mod h1:jl5iWTm0/hd5PjEYEOuwAJ57L/CibdZfrqZ5XA5GrCk= github.com/google/pprof v0.0.0-20250317173921-a4b03ec1a45e/go.mod h1:boTsfXsheKC2y+lKOCMpSfarhxDeIzfZG1jqGcPl3cA=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= 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/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg= github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg=
github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
github.com/grpc-ecosystem/grpc-gateway/v2 v2.28.0 h1:HWRh5R2+9EifMyIHV7ZV+MIZqgz+PMpZ14Jynv3O2Zs=
github.com/grpc-ecosystem/grpc-gateway/v2 v2.28.0/go.mod h1:JfhWUomR1baixubs02l85lZYYOm7LV6om4ceouMv45c=
github.com/hashicorp/golang-lru/v2 v2.0.7 h1:a+bsQ5rvGLjzHuww6tVxozPZFVghXaHOwFs4luLUK2k= github.com/hashicorp/golang-lru/v2 v2.0.7 h1:a+bsQ5rvGLjzHuww6tVxozPZFVghXaHOwFs4luLUK2k=
github.com/hashicorp/golang-lru/v2 v2.0.7/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM= github.com/hashicorp/golang-lru/v2 v2.0.7/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM=
github.com/ianlancetaylor/demangle v0.0.0-20260505044615-1ff4bf46051f h1:NW3E2QSchEk63/fjeEvWOa2cE02FSv9ox//VE/N4c8g= github.com/ianlancetaylor/demangle v0.0.0-20240805132620-81f5be970eca h1:T54Ema1DU8ngI+aef9ZhAhNGQhcRTrWxVeG07F+c/Rw=
github.com/ianlancetaylor/demangle v0.0.0-20260505044615-1ff4bf46051f/go.mod h1:gx7rwoVhcfuVKG5uya9Hs3Sxj7EIvldVofAWIUtGouw= github.com/ianlancetaylor/demangle v0.0.0-20240805132620-81f5be970eca/go.mod h1:gx7rwoVhcfuVKG5uya9Hs3Sxj7EIvldVofAWIUtGouw=
github.com/icholy/digest v1.1.0 h1:HfGg9Irj7i+IX1o1QAmPfIBNu/Q5A5Tu3n/MED9k9H4= github.com/icholy/digest v1.1.0 h1:HfGg9Irj7i+IX1o1QAmPfIBNu/Q5A5Tu3n/MED9k9H4=
github.com/icholy/digest v1.1.0/go.mod h1:QNrsSGQ5v7v9cReDI0+eyjsXGUoRSUZQHeQ5C4XLa0Y= github.com/icholy/digest v1.1.0/go.mod h1:QNrsSGQ5v7v9cReDI0+eyjsXGUoRSUZQHeQ5C4XLa0Y=
github.com/jwetzell/artnet-go v0.3.0 h1:MDHElOSbpgmIbYGHO9SoxsT7ynrYtV5/UHGT82geHk0= github.com/jwetzell/artnet-go v0.2.1 h1:iYTKWcwYrF5kBkYfkw2UbWvoueeA23iKEn7fR27mWZE=
github.com/jwetzell/artnet-go v0.3.0/go.mod h1:zqRsSsrd6rPwJacwscPteDqGIqevjZ3Z6dpp092VJ1w= github.com/jwetzell/artnet-go v0.2.1/go.mod h1:gli97Z32a0kMkZ6taoTiK7/lqHcF/dhiGjGJdx/PxqA=
github.com/jwetzell/free-d-go v0.2.0 h1:8WQW4du8Sf3d18aUzk9n5jZtHE6WdDQHDFjkFffvycc= github.com/jwetzell/free-d-go v0.1.0 h1:xHt6dvyit98X+OC3jVzV0aLidxbyzi3vI9QiYkteEtA=
github.com/jwetzell/free-d-go v0.2.0/go.mod h1:yHuRy51NeDfN26eJ6wzZgys8qyQiS7hkYILw8yF+UJ4= github.com/jwetzell/free-d-go v0.1.0/go.mod h1:KmrkooRARRaxJTBSPvwt/6IMAIaHH1R8bSA8cwbbELw=
github.com/jwetzell/osc-go v0.3.0 h1:z75TxuQSEmdcmZ56OAepkDa3m88SdZh//3m4nBb/XZI= github.com/jwetzell/osc-go v0.2.0 h1:4as+BYCeZhEddFczGveP5yZZxvY728Uavz+ZSLZfOII=
github.com/jwetzell/osc-go v0.3.0/go.mod h1:kCs329JxY6Qjga08tRQ/Gl0PqhgQzLIMpOhm6uszvIc= github.com/jwetzell/osc-go v0.2.0/go.mod h1:D3ZIXYB12bt4S35lKFUqgCFbF1Y+9Ld0sOhHA9mGZZM=
github.com/jwetzell/psn-go v0.3.0 h1:WVpCEmExYE8a+I5hQak5jNJJp2x35VdGX/VuMUKPmhY= github.com/jwetzell/psn-go v0.3.0 h1:WVpCEmExYE8a+I5hQak5jNJJp2x35VdGX/VuMUKPmhY=
github.com/jwetzell/psn-go v0.3.0/go.mod h1:bcEAeti4sQM375buujb3mIfmUstD4Aby18gq3ENb6+o= github.com/jwetzell/psn-go v0.3.0/go.mod h1:bcEAeti4sQM375buujb3mIfmUstD4Aby18gq3ENb6+o=
github.com/klauspost/compress v1.19.0 h1:sXLILfc9jV2QYWkzFOPWStmcUVH2RHEB1JCdY2oVvCQ= github.com/klauspost/compress v1.18.4 h1:RPhnKRAQ4Fh8zU2FY/6ZFDwTVTxgJ/EMydqSTzE9a2c=
github.com/klauspost/compress v1.19.0/go.mod h1:cwPg85FWrGar70rWktvGQj8/hthj3wpl0PGDogxkrSQ= github.com/klauspost/compress v1.18.4/go.mod h1:R0h/fSBs8DE4ENlcrlib3PsXS61voFxhIs2DeRhCvJ4=
github.com/klauspost/cpuid/v2 v2.2.10 h1:tBs3QSyvjDyFTq3uoc/9xFpCuOsJQFNPiAhYdw2skhE= github.com/klauspost/cpuid/v2 v2.0.9 h1:lgaqFMSdTdQYdZ04uHyN2d/eKdOMyi2YLSvlQIBFYa4=
github.com/klauspost/cpuid/v2 v2.2.10/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0= github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/mattn/go-isatty v0.0.24 h1:tGZZoVgT/KiqK1c8ocVLeDS8BSWMRd47J3Lbz7vsReI= github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
github.com/mattn/go-isatty v0.0.24/go.mod h1:nMCL3Zebbrt45jsMDgnfIwz6ydEQApk5oEI3HqDio6A= github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/minio/highwayhash v1.0.4 h1:asJizugGgchQod2ja9NJlGOWq4s7KsAWr5XUc9Clgl4= github.com/minio/highwayhash v1.0.4-0.20251030100505-070ab1a87a76 h1:KGuD/pM2JpL9FAYvBrnBBeENKZNh6eNtjqytV6TYjnk=
github.com/minio/highwayhash v1.0.4/go.mod h1:GGYsuwP/fPD6Y9hMiXuapVvlIUEhFhMTh0rxU3ik1LQ= github.com/minio/highwayhash v1.0.4-0.20251030100505-070ab1a87a76/go.mod h1:GGYsuwP/fPD6Y9hMiXuapVvlIUEhFhMTh0rxU3ik1LQ=
github.com/nats-io/jwt/v2 v2.8.2 h1:XXRgB60MSTnqsRwejQurVDs/hcv2dkt+86GjI+I/bMc= github.com/nats-io/jwt/v2 v2.8.1 h1:V0xpGuD/N8Mi+fQNDynXohVvp7ZztevW5io8CUWlPmU=
github.com/nats-io/jwt/v2 v2.8.2/go.mod h1:Ag/56sq9OblL4JgdYufDd16Egb17Kr/8WwwuO/forVc= github.com/nats-io/jwt/v2 v2.8.1/go.mod h1:nWnOEEiVMiKHQpnAy4eXlizVEtSfzacZ1Q43LIRavZg=
github.com/nats-io/nats-server/v2 v2.14.4 h1:efgjZ8cdExAKRuqSg8UPJFprb+l7NlBtSDPhDlw3rO4= github.com/nats-io/nats-server/v2 v2.12.6 h1:Egbx9Vl7Ch8wTtpXPGqbehkZ+IncKqShUxvrt1+Enc8=
github.com/nats-io/nats-server/v2 v2.14.4/go.mod h1:BltdpOYestjbtQSnVO2zGHdg5SGBZjt+GYTgB9LZq/I= github.com/nats-io/nats-server/v2 v2.12.6/go.mod h1:4HPlrvtmSO3yd7KcElDNMx9kv5EBJBnJJzQPptXlheo=
github.com/nats-io/nats.go v1.52.0 h1:n3avV4VBsCgsdwh71TppsTwtv+QdPs7ntSKM8qJLGsc= github.com/nats-io/nats.go v1.49.0 h1:yh/WvY59gXqYpgl33ZI+XoVPKyut/IcEaqtsiuTJpoE=
github.com/nats-io/nats.go v1.52.0/go.mod h1:26HypzazeOkyO3/mqd1zZd53STJN0EjCYF9Uy2ZOBno= github.com/nats-io/nats.go v1.49.0/go.mod h1:fDCn3mN5cY8HooHwE2ukiLb4p4G4ImmzvXyJt+tGwdw=
github.com/nats-io/nkeys v0.4.16 h1:rd5oAuLOb8mnAycB0xleuEBNS1pVVnN0fv/FF34Eypg= github.com/nats-io/nkeys v0.4.15 h1:JACV5jRVO9V856KOapQ7x+EY8Jo3qw1vJt/9Jpwzkk4=
github.com/nats-io/nkeys v0.4.16/go.mod h1:llLgWoI0o4z/Q57q2R1kHfmocyhGV6VG/U18Glg1Afs= github.com/nats-io/nkeys v0.4.15/go.mod h1:CpMchTXC9fxA5zrMo4KpySxNjiDVvr8ANOSZdiNfUrs=
github.com/nats-io/nuid v1.0.1 h1:5iA8DT8V7q8WK2EScv2padNa/rTESc1KdnPw4TC2paw= github.com/nats-io/nuid v1.0.1 h1:5iA8DT8V7q8WK2EScv2padNa/rTESc1KdnPw4TC2paw=
github.com/nats-io/nuid v1.0.1/go.mod h1:19wcPz3Ph3q0Jbyiqsd0kePYG7A95tJPxeL+1OSON2c= github.com/nats-io/nuid v1.0.1/go.mod h1:19wcPz3Ph3q0Jbyiqsd0kePYG7A95tJPxeL+1OSON2c=
github.com/ncruces/go-strftime v1.0.0 h1:HMFp8mLCTPp341M/ZnA4qaf7ZlsbTc+miZjCLOFAw7w= github.com/ncruces/go-strftime v1.0.0 h1:HMFp8mLCTPp341M/ZnA4qaf7ZlsbTc+miZjCLOFAw7w=
@@ -90,16 +105,18 @@ github.com/pion/randutil v0.1.0 h1:CFG1UdESneORglEsnimhUjf33Rwjubwj6xfiOXBa3mA=
github.com/pion/randutil v0.1.0/go.mod h1:XcJrSMMbbMRhASFVOlj/5hQial/Y8oH/HVo7TBZq+j8= github.com/pion/randutil v0.1.0/go.mod h1:XcJrSMMbbMRhASFVOlj/5hQial/Y8oH/HVo7TBZq+j8=
github.com/pion/rtcp v1.2.16 h1:fk1B1dNW4hsI78XUCljZJlC4kZOPk67mNRuQ0fcEkSo= github.com/pion/rtcp v1.2.16 h1:fk1B1dNW4hsI78XUCljZJlC4kZOPk67mNRuQ0fcEkSo=
github.com/pion/rtcp v1.2.16/go.mod h1:/as7VKfYbs5NIb4h6muQ35kQF/J0ZVNz2Z3xKoCBYOo= github.com/pion/rtcp v1.2.16/go.mod h1:/as7VKfYbs5NIb4h6muQ35kQF/J0ZVNz2Z3xKoCBYOo=
github.com/pion/rtp v1.10.2 h1:l+f6tTDcAH6xwepaAoW791ddhuYsJlqRATOzirO04Mo= github.com/pion/rtp v1.8.26 h1:VB+ESQFQhBXFytD+Gk8cxB6dXeVf2WQzg4aORvAvAAc=
github.com/pion/rtp v1.10.2/go.mod h1:Au8fc6cEByy8RLTwKTQTEeQqDB/SJDxwL4mZuxYA5Pk= github.com/pion/rtp v1.8.26/go.mod h1:rF5nS1GqbR7H/TCpKwylzeq6yDM+MM6k+On5EgeThEM=
github.com/pion/srtp/v3 v3.0.10 h1:tFirkpBb3XccP5VEXLi50GqXhv5SKPxqrdlhDCJlZrQ= github.com/pion/srtp/v3 v3.0.9 h1:lRGF4G61xxj+m/YluB3ZnBpiALSri2lTzba0kGZMrQY=
github.com/pion/srtp/v3 v3.0.10/go.mod h1:3mOTIB0cq9qlbn59V4ozvv9ClW/BSEbRp4cY0VtaR7M= github.com/pion/srtp/v3 v3.0.9/go.mod h1:E+AuWd7Ug2Fp5u38MKnhduvpVkveXJX6J4Lq4rxUYt8=
github.com/pion/transport/v3 v3.1.1 h1:Tr684+fnnKlhPceU+ICdrw6KKkTms+5qHMgw6bIkYOM=
github.com/pion/transport/v3 v3.1.1/go.mod h1:+c2eewC5WJQHiAA46fkMMzoYZSuGzA/7E2FPrOYHctQ=
github.com/pion/transport/v4 v4.0.1 h1:sdROELU6BZ63Ab7FrOLn13M6YdJLY20wldXW2Cu2k8o= github.com/pion/transport/v4 v4.0.1 h1:sdROELU6BZ63Ab7FrOLn13M6YdJLY20wldXW2Cu2k8o=
github.com/pion/transport/v4 v4.0.1/go.mod h1:nEuEA4AD5lPdcIegQDpVLgNoDGreqM/YqmEx3ovP4jM= github.com/pion/transport/v4 v4.0.1/go.mod h1:nEuEA4AD5lPdcIegQDpVLgNoDGreqM/YqmEx3ovP4jM=
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/redis/go-redis/v9 v9.22.0 h1:laDvpYXTJtZLloinw1fA5Kqd6HAEH2XKxOkG/PDq2F0= github.com/redis/go-redis/v9 v9.18.0 h1:pMkxYPkEbMPwRdenAzUNyFNrDgHx9U+DrBabWNfSRQs=
github.com/redis/go-redis/v9 v9.22.0/go.mod h1:y2g0Wj8rQvuK0ELM+oxSudcLtC09JScs98I/X9gRWY4= github.com/redis/go-redis/v9 v9.18.0/go.mod h1:k3ufPphLU5YXwNTUcCRXGxUoF1fqxnhFQmscfkCoDA0=
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE= github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE=
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo= github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ= github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=
@@ -108,42 +125,70 @@ github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
github.com/tetratelabs/wabin v0.0.0-20230304001439-f6f874872834 h1:ZF+QBjOI+tILZjBaFj3HgFonKXUcwgJ4djLb6i42S3Q= github.com/tetratelabs/wabin v0.0.0-20230304001439-f6f874872834 h1:ZF+QBjOI+tILZjBaFj3HgFonKXUcwgJ4djLb6i42S3Q=
github.com/tetratelabs/wabin v0.0.0-20230304001439-f6f874872834/go.mod h1:m9ymHTgNSEjuxvw8E7WWe4Pl4hZQHXONY8wE6dMLaRk= github.com/tetratelabs/wabin v0.0.0-20230304001439-f6f874872834/go.mod h1:m9ymHTgNSEjuxvw8E7WWe4Pl4hZQHXONY8wE6dMLaRk=
github.com/tetratelabs/wazero v1.11.0 h1:+gKemEuKCTevU4d7ZTzlsvgd1uaToIDtlQlmNbwqYhA= github.com/tetratelabs/wazero v1.9.0 h1:IcZ56OuxrtaEz8UYNRHBrUa9bYeX9oVY93KspZZBf/I=
github.com/tetratelabs/wazero v1.11.0/go.mod h1:eV28rsN8Q+xwjogd7f4/Pp4xFxO7uOGbLcD/LzB1wiU= github.com/tetratelabs/wazero v1.9.0/go.mod h1:TSbcXCfFP0L2FGkRPxHphadXPjo1T6W+CseNNY7EkjM=
github.com/urfave/cli/v3 v3.10.1 h1:7Kx9H50hrHbRbyxgO1KP6/BcbiGRz0uYh5YyQ30JEEY= github.com/urfave/cli/v3 v3.7.0 h1:AGSnbUyjtLiM+WJUb4dzXKldl/gL+F8OwmRDtVr6g2U=
github.com/urfave/cli/v3 v3.10.1/go.mod h1:ysVLtOEmg2tOy6PknnYVhDoouyC/6N42TMeoMzskhso= github.com/urfave/cli/v3 v3.7.0/go.mod h1:ysVLtOEmg2tOy6PknnYVhDoouyC/6N42TMeoMzskhso=
github.com/zaf/g711 v1.4.0 h1:XZYkjjiAg9QTBnHqEg37m2I9q3IIDv5JRYXs2N8ma7c= github.com/zaf/g711 v1.4.0 h1:XZYkjjiAg9QTBnHqEg37m2I9q3IIDv5JRYXs2N8ma7c=
github.com/zaf/g711 v1.4.0/go.mod h1:eCDXt3dSp/kYYAoooba7ukD/Q75jvAaS4WOMr0l1Roo= github.com/zaf/g711 v1.4.0/go.mod h1:eCDXt3dSp/kYYAoooba7ukD/Q75jvAaS4WOMr0l1Roo=
github.com/zeebo/xxh3 v1.1.0 h1:s7DLGDK45Dyfg7++yxI0khrfwq9661w9EN78eP/UZVs= github.com/zeebo/xxh3 v1.0.2 h1:xZmwmqxHZA8AI603jOQ0tMqmBr9lPeFwGg6d+xy9DC0=
github.com/zeebo/xxh3 v1.1.0/go.mod h1:IisAie1LELR4xhVinxWS5+zf1lA4p0MW4T+w+W07F5s= github.com/zeebo/xxh3 v1.0.2/go.mod h1:5NWz9Sef7zIDm2JHfFlcQvNekmcEl9ekUZQQKCYaDcA=
gitlab.com/gomidi/midi/v2 v2.3.24 h1:afkq5nhlzKvZaj9QK80YbK8tH3lIlKLnPPP9HxxD7Do= gitlab.com/gomidi/midi/v2 v2.3.23 h1:P8NxV4EzV9c+BjpwTeB+G/qa+Xdq/UTazS2fKxY0O0g=
gitlab.com/gomidi/midi/v2 v2.3.24/go.mod h1:jDpP4O4skYi+7iVwt6Zyp18bd2M4hkjtMuw2cmgKgfw= gitlab.com/gomidi/midi/v2 v2.3.23/go.mod h1:jDpP4O4skYi+7iVwt6Zyp18bd2M4hkjtMuw2cmgKgfw=
go.bug.st/serial v1.8.0 h1:ZtnmN8aYXtPlTghwSvDWPHKBHL9TM6oFDa+KpSn4SQE= go.bug.st/serial v1.6.4 h1:7FmqNPgVp3pu2Jz5PoPtbZ9jJO5gnEnZIvnI1lzve8A=
go.bug.st/serial v1.8.0/go.mod h1:d0MmS16Qt9b1m06yoYRNUXhRRTJV5Qg2S5EKqQtnayQ= go.bug.st/serial v1.6.4/go.mod h1:nofMJxTeNVny/m6+KaafC6vJGj3miwQZ6vW4BZUGJPI=
go.opentelemetry.io/proto/otlp v1.10.0 h1:IQRWgT5srOCYfiWnpqUYz9CVmbO8bFmKcwYxpuCSL2g= go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64=
go.opentelemetry.io/proto/otlp v1.10.0/go.mod h1:/CV4QoCR/S9yaPj8utp3lvQPoqMtxXdzn7ozvvozVqk= go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y=
go.opentelemetry.io/otel v1.42.0 h1:lSQGzTgVR3+sgJDAU/7/ZMjN9Z+vUip7leaqBKy4sho=
go.opentelemetry.io/otel v1.42.0/go.mod h1:lJNsdRMxCUIWuMlVJWzecSMuNjE7dOYyWlqOXWkdqCc=
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.42.0 h1:THuZiwpQZuHPul65w4WcwEnkX2QIuMT+UFoOrygtoJw=
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.42.0/go.mod h1:J2pvYM5NGHofZ2/Ru6zw/TNWnEQp5crgyDeSrYpXkAw=
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.42.0 h1:uLXP+3mghfMf7XmV4PkGfFhFKuNWoCvvx5wP/wOXo0o=
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.42.0/go.mod h1:v0Tj04armyT59mnURNUJf7RCKcKzq+lgJs6QSjHjaTc=
go.opentelemetry.io/otel/metric v1.42.0 h1:2jXG+3oZLNXEPfNmnpxKDeZsFI5o4J+nz6xUlaFdF/4=
go.opentelemetry.io/otel/metric v1.42.0/go.mod h1:RlUN/7vTU7Ao/diDkEpQpnz3/92J9ko05BIwxYa2SSI=
go.opentelemetry.io/otel/sdk v1.42.0 h1:LyC8+jqk6UJwdrI/8VydAq/hvkFKNHZVIWuslJXYsDo=
go.opentelemetry.io/otel/sdk v1.42.0/go.mod h1:rGHCAxd9DAph0joO4W6OPwxjNTYWghRWmkHuGbayMts=
go.opentelemetry.io/otel/sdk/metric v1.42.0 h1:D/1QR46Clz6ajyZ3G8SgNlTJKBdGp84q9RKCAZ3YGuA=
go.opentelemetry.io/otel/sdk/metric v1.42.0/go.mod h1:Ua6AAlDKdZ7tdvaQKfSmnFTdHx37+J4ba8MwVCYM5hc=
go.opentelemetry.io/otel/trace v1.42.0 h1:OUCgIPt+mzOnaUTpOQcBiM/PLQ/Op7oq6g4LenLmOYY=
go.opentelemetry.io/otel/trace v1.42.0/go.mod h1:f3K9S+IFqnumBkKhRJMeaZeNk9epyhnCmQh/EysQCdc=
go.opentelemetry.io/proto/otlp v1.9.0 h1:l706jCMITVouPOqEnii2fIAuO3IVGBRPV5ICjceRb/A=
go.opentelemetry.io/proto/otlp v1.9.0/go.mod h1:xE+Cx5E/eEHw+ISFkwPLwCZefwVjY+pqKg1qcK03+/4=
go.uber.org/atomic v1.11.0 h1:ZvwS0R+56ePWxUNi+Atn9dWONBPp/AUETXlHW0DxSjE= go.uber.org/atomic v1.11.0 h1:ZvwS0R+56ePWxUNi+Atn9dWONBPp/AUETXlHW0DxSjE=
go.uber.org/atomic v1.11.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0= go.uber.org/atomic v1.11.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0=
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
go.yaml.in/yaml/v2 v2.4.2 h1:DzmwEr2rDGHl7lsFgAHxmNz/1NlQ7xLIrlN2h5d1eGI= go.yaml.in/yaml/v2 v2.4.2 h1:DzmwEr2rDGHl7lsFgAHxmNz/1NlQ7xLIrlN2h5d1eGI=
go.yaml.in/yaml/v2 v2.4.2/go.mod h1:081UH+NErpNdqlCXm3TtEran0rJZGxAYx9hb/ELlsPU= go.yaml.in/yaml/v2 v2.4.2/go.mod h1:081UH+NErpNdqlCXm3TtEran0rJZGxAYx9hb/ELlsPU=
go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc= go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc=
go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg= go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg=
golang.org/x/crypto v0.54.0 h1:YLIA59K4fiNzHzjnZt2tUJQjQtUWfWbeHBqKtk3eScw= golang.org/x/crypto v0.49.0 h1:+Ng2ULVvLHnJ/ZFEq4KdcDd/cfjrrjjNSXNzxg0Y4U4=
golang.org/x/crypto v0.54.0/go.mod h1:KWL8ny2AZdGR2cWmzeHrp2azQPGogOv+HeQaVEXC2dk= golang.org/x/crypto v0.49.0/go.mod h1:ErX4dUh2UM+CFYiXZRTcMpEcN8b/1gxEuv3nODoYtCA=
golang.org/x/mod v0.37.0 h1:vF1DjpVEshcIqoEaauuHebaLk1O1forxjxBaVn884JQ= golang.org/x/mod v0.33.0 h1:tHFzIWbBifEmbwtGz65eaWyGiGZatSrT9prnU8DbVL8=
golang.org/x/mod v0.37.0/go.mod h1:m8S8VeM9r4dzDwjrKO0a1sZP3YjeMamRRlD+fmR2Q/0= golang.org/x/mod v0.33.0/go.mod h1:swjeQEj+6r7fODbD2cqrnje9PnziFuw4bmLbBZFrQ5w=
golang.org/x/net v0.56.0 h1:Rw8j/hFzGvJUZwNBXnAtf5sVDVt+65SK2C7IxCxZt5o= golang.org/x/net v0.51.0 h1:94R/GTO7mt3/4wIKpcR5gkGmRLOuE/2hNGeWq/GBIFo=
golang.org/x/net v0.56.0/go.mod h1:D3Ku6r+V6JROoZK144D2XfMHFcMq/0zSfLelVTCFKec= golang.org/x/net v0.51.0/go.mod h1:aamm+2QF5ogm02fjy5Bb7CQ0WMt1/WVM7FtyaTLlA9Y=
golang.org/x/sync v0.21.0 h1:HLII4xRRTtCRkxYp4HNFF0Js/Og6q2i++KXbg0gHCwM= golang.org/x/sync v0.20.0 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4=
golang.org/x/sync v0.21.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0= golang.org/x/sync v0.20.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.21.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.21.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.47.0 h1:o7XGOvZQCADBQQ4Y7VNq2dRWQR7JmOUW8Kxx4ZsNgWs= golang.org/x/sys v0.42.0 h1:omrd2nAlyT5ESRdCLYdm3+fMfNFE/+Rf4bDIQImRJeo=
golang.org/x/sys v0.47.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= golang.org/x/sys v0.42.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
golang.org/x/text v0.35.0 h1:JOVx6vVDFokkpaq1AEptVzLTpDe9KGpj5tR4/X+ybL8=
golang.org/x/text v0.35.0/go.mod h1:khi/HExzZJ2pGnjenulevKNX1W67CUy0AsXcNubPGCA=
golang.org/x/time v0.15.0 h1:bbrp8t3bGUeFOx08pvsMYRTCVSMk89u4tKbNOZbp88U= golang.org/x/time v0.15.0 h1:bbrp8t3bGUeFOx08pvsMYRTCVSMk89u4tKbNOZbp88U=
golang.org/x/time v0.15.0/go.mod h1:Y4YMaQmXwGQZoFaVFk4YpCt4FLQMYKZe9oeV/f4MSno= golang.org/x/time v0.15.0/go.mod h1:Y4YMaQmXwGQZoFaVFk4YpCt4FLQMYKZe9oeV/f4MSno=
golang.org/x/tools v0.47.0 h1:7Kn5x/d1svx/PzryTsqeoZN4TZwqeH5pGWjefhLi/1Q= golang.org/x/tools v0.42.0 h1:uNgphsn75Tdz5Ji2q36v/nsFSfR/9BRFvqhGBaJGd5k=
golang.org/x/tools v0.47.0/go.mod h1:dFHnyTvFWY212G+h7ZY4Vsp/K3U4/7W9TyVaAul8uCA= golang.org/x/tools v0.42.0/go.mod h1:Ma6lCIwGZvHK6XtgbswSoWroEkhugApmsXyrUmBhfr0=
gonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk=
gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E=
google.golang.org/genproto/googleapis/api v0.0.0-20260209200024-4cfbd4190f57 h1:JLQynH/LBHfCTSbDWl+py8C+Rg/k1OVH3xfcaiANuF0=
google.golang.org/genproto/googleapis/api v0.0.0-20260209200024-4cfbd4190f57/go.mod h1:kSJwQxqmFXeo79zOmbrALdflXQeAYcUbgS7PbpMknCY=
google.golang.org/genproto/googleapis/rpc v0.0.0-20260209200024-4cfbd4190f57 h1:mWPCjDEyshlQYzBpMNHaEof6UX1PmHcaUODUywQ0uac=
google.golang.org/genproto/googleapis/rpc v0.0.0-20260209200024-4cfbd4190f57/go.mod h1:j9x/tPzZkyxcgEFkiKEEGxfvyumM01BEtsW8xzOahRQ=
google.golang.org/grpc v1.79.2 h1:fRMD94s2tITpyJGtBBn7MkMseNpOZU8ZxgC3MMBaXRU=
google.golang.org/grpc v1.79.2/go.mod h1:KmT0Kjez+0dde/v2j9vzwoAScgEPx/Bw1CYChhHLrHQ=
google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE= google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE=
google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
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=
@@ -155,34 +200,34 @@ gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gotest.tools/v3 v3.5.2 h1:7koQfIKdy+I8UTetycgUqXWSDwpgv193Ka+qRsmBY8Q= gotest.tools/v3 v3.5.2 h1:7koQfIKdy+I8UTetycgUqXWSDwpgv193Ka+qRsmBY8Q=
gotest.tools/v3 v3.5.2/go.mod h1:LtdLGcnqToBH83WByAAi/wiwSFCArdFIUV/xxN4pcjA= gotest.tools/v3 v3.5.2/go.mod h1:LtdLGcnqToBH83WByAAi/wiwSFCArdFIUV/xxN4pcjA=
modernc.org/cc/v4 v4.29.1 h1:MKgdCV3WykTSPqpVrnxdEDS0HEd2FHpKZDzxzU5LyeI= modernc.org/cc/v4 v4.27.1 h1:9W30zRlYrefrDV2JE2O8VDtJ1yPGownxciz5rrbQZis=
modernc.org/cc/v4 v4.29.1/go.mod h1:OnovgIhbbMXMu1aISnJ0wvVD1KnW+cAUJkIrAWh+kVI= modernc.org/cc/v4 v4.27.1/go.mod h1:uVtb5OGqUKpoLWhqwNQo/8LwvoiEBLvZXIQ/SmO6mL0=
modernc.org/ccgo/v4 v4.34.6 h1:sBgfIwyN0TQ9C5hwIeuqyeAKyMWnbvj2fvpF4L11uzU= modernc.org/ccgo/v4 v4.32.0 h1:hjG66bI/kqIPX1b2yT6fr/jt+QedtP2fqojG2VrFuVw=
modernc.org/ccgo/v4 v4.34.6/go.mod h1:SZ8YcN9NG7XVsQYdm6jYBvi8PQP1qi+kqB6OhjqI3Fk= modernc.org/ccgo/v4 v4.32.0/go.mod h1:6F08EBCx5uQc38kMGl+0Nm0oWczoo1c7cgpzEry7Uc0=
modernc.org/fileutil v1.4.0 h1:j6ZzNTftVS054gi281TyLjHPp6CPHr2KCxEXjEbD6SM= modernc.org/fileutil v1.4.0 h1:j6ZzNTftVS054gi281TyLjHPp6CPHr2KCxEXjEbD6SM=
modernc.org/fileutil v1.4.0/go.mod h1:EqdKFDxiByqxLk8ozOxObDSfcVOv/54xDs/DUHdvCUU= modernc.org/fileutil v1.4.0/go.mod h1:EqdKFDxiByqxLk8ozOxObDSfcVOv/54xDs/DUHdvCUU=
modernc.org/gc/v2 v2.6.5 h1:nyqdV8q46KvTpZlsw66kWqwXRHdjIlJOhG6kxiV/9xI= modernc.org/gc/v2 v2.6.5 h1:nyqdV8q46KvTpZlsw66kWqwXRHdjIlJOhG6kxiV/9xI=
modernc.org/gc/v2 v2.6.5/go.mod h1:YgIahr1ypgfe7chRuJi2gD7DBQiKSLMPgBQe9oIiito= modernc.org/gc/v2 v2.6.5/go.mod h1:YgIahr1ypgfe7chRuJi2gD7DBQiKSLMPgBQe9oIiito=
modernc.org/gc/v3 v3.1.5 h1:21ldfPfRYE31Tb7B3mwAK8gy1AxP4+dKjrOQPfqakoc= modernc.org/gc/v3 v3.1.2 h1:ZtDCnhonXSZexk/AYsegNRV1lJGgaNZJuKjJSWKyEqo=
modernc.org/gc/v3 v3.1.5/go.mod h1:HFK/6AGESC7Ex+EZJhJ2Gni6cTaYpSMmU/cT9RmlfYY= modernc.org/gc/v3 v3.1.2/go.mod h1:HFK/6AGESC7Ex+EZJhJ2Gni6cTaYpSMmU/cT9RmlfYY=
modernc.org/goabi0 v0.2.0 h1:HvEowk7LxcPd0eq6mVOAEMai46V+i7Jrj13t4AzuNks= modernc.org/goabi0 v0.2.0 h1:HvEowk7LxcPd0eq6mVOAEMai46V+i7Jrj13t4AzuNks=
modernc.org/goabi0 v0.2.0/go.mod h1:CEFRnnJhKvWT1c1JTI3Avm+tgOWbkOu5oPA8eH8LnMI= modernc.org/goabi0 v0.2.0/go.mod h1:CEFRnnJhKvWT1c1JTI3Avm+tgOWbkOu5oPA8eH8LnMI=
modernc.org/libc v1.74.4 h1:fX1Omw4o2/1C2iRkkIsrQTasJQldLhRmuPreXLoWs9k= modernc.org/libc v1.70.0 h1:U58NawXqXbgpZ/dcdS9kMshu08aiA6b7gusEusqzNkw=
modernc.org/libc v1.74.4/go.mod h1:eeQAS9W3sZeKYMFubydxJpII9ybHWshk+7or7bLG9co= modernc.org/libc v1.70.0/go.mod h1:OVmxFGP1CI/Z4L3E0Q3Mf1PDE0BucwMkcXjjLntvHJo=
modernc.org/libquickjs v0.13.1 h1:uCb9AYyEyL1JS2dnKtLZKWH/vDb36cx1Mr1SIdbzb8A= modernc.org/libquickjs v0.12.3 h1:2IU9B6njBmce2PuYttJDkXeoLRV9WnvgP+eU5HAC8YI=
modernc.org/libquickjs v0.13.1/go.mod h1:tCEsA1Zda1+C5JagFIJGqjYZOxDPFOUYjK3dGhEeq0g= modernc.org/libquickjs v0.12.3/go.mod h1:iCsgVxnHTX3i0YPxxHBmJk0GLA5sVUHXWI/090UXgeE=
modernc.org/mathutil v1.7.1 h1:GCZVGXdaN8gTqB1Mf/usp1Y/hSqgI2vAGGP4jZMCxOU= modernc.org/mathutil v1.7.1 h1:GCZVGXdaN8gTqB1Mf/usp1Y/hSqgI2vAGGP4jZMCxOU=
modernc.org/mathutil v1.7.1/go.mod h1:4p5IwJITfppl0G4sUEDtCr4DthTaT47/N3aT6MhfgJg= modernc.org/mathutil v1.7.1/go.mod h1:4p5IwJITfppl0G4sUEDtCr4DthTaT47/N3aT6MhfgJg=
modernc.org/memory v1.11.0 h1:o4QC8aMQzmcwCK3t3Ux/ZHmwFPzE6hf2Y5LbkRs+hbI= modernc.org/memory v1.11.0 h1:o4QC8aMQzmcwCK3t3Ux/ZHmwFPzE6hf2Y5LbkRs+hbI=
modernc.org/memory v1.11.0/go.mod h1:/JP4VbVC+K5sU2wZi9bHoq2MAkCnrt2r98UGeSK7Mjw= modernc.org/memory v1.11.0/go.mod h1:/JP4VbVC+K5sU2wZi9bHoq2MAkCnrt2r98UGeSK7Mjw=
modernc.org/opt v0.2.0 h1:tGyef5ApycA7FSEOMraay9SaTk5zmbx7Tu+cJs4QKZg= modernc.org/opt v0.1.4 h1:2kNGMRiUjrp4LcaPuLY2PzUfqM/w9N23quVwhKt5Qm8=
modernc.org/opt v0.2.0/go.mod h1:03fq9lsNfvkYSfxrfUhZCWPk1lm4cq4N+Bh//bEtgns= modernc.org/opt v0.1.4/go.mod h1:03fq9lsNfvkYSfxrfUhZCWPk1lm4cq4N+Bh//bEtgns=
modernc.org/quickjs v0.23.0 h1:k+px+rFrMqHm2LPMvrd+vCR+8m6eogiTkROz+YJHkj8= modernc.org/quickjs v0.17.1 h1:CbYnbTf7ksZk9YZ1rRM2Ab1Zfi+X6s50kXiOhpd2NIg=
modernc.org/quickjs v0.23.0/go.mod h1:bFvjVk4JGlbqLYXVFt72YvwdnHbwtNIE9nQ+eh4fdi0= modernc.org/quickjs v0.17.1/go.mod h1:hATT7DIJc33I5Q/Fjffhm0tpUHNSqdKHma/ossibTA0=
modernc.org/sortutil v1.2.1 h1:+xyoGf15mM3NMlPDnFqrteY07klSFxLElE2PVuWIJ7w= modernc.org/sortutil v1.2.1 h1:+xyoGf15mM3NMlPDnFqrteY07klSFxLElE2PVuWIJ7w=
modernc.org/sortutil v1.2.1/go.mod h1:7ZI3a3REbai7gzCLcotuw9AC4VZVpYMjDzETGsSMqJE= modernc.org/sortutil v1.2.1/go.mod h1:7ZI3a3REbai7gzCLcotuw9AC4VZVpYMjDzETGsSMqJE=
modernc.org/sqlite v1.56.0 h1:/D8e2RfFqoy/Zc6PuC76U28zFwmI/sYx1Kjm4yEn9e0= modernc.org/sqlite v1.47.0 h1:R1XyaNpoW4Et9yly+I2EeX7pBza/w+pmYee/0HJDyKk=
modernc.org/sqlite v1.56.0/go.mod h1:yCJ2cmAaIkHQ25oXWrF8H4O1lIfPYPR26yCEDj2P3pQ= modernc.org/sqlite v1.47.0/go.mod h1:hWjRO6Tj/5Ik8ieqxQybiEOUXy0NJFNp2tpvVpKlvig=
modernc.org/strutil v1.2.1 h1:UneZBkQA+DX2Rp35KcM69cSsNES9ly8mQWD71HKlOA0= modernc.org/strutil v1.2.1 h1:UneZBkQA+DX2Rp35KcM69cSsNES9ly8mQWD71HKlOA0=
modernc.org/strutil v1.2.1/go.mod h1:EHkiggD70koQxjVdSBM3JKM7k6L0FbGE5eymy9i3B9A= modernc.org/strutil v1.2.1/go.mod h1:EHkiggD70koQxjVdSBM3JKM7k6L0FbGE5eymy9i3B9A=
modernc.org/token v1.1.0 h1:Xl7Ap9dKaEs5kLoOQeQmPWevfnk/DM5qcLcYlA8ys6Y= modernc.org/token v1.1.0 h1:Xl7Ap9dKaEs5kLoOQeQmPWevfnk/DM5qcLcYlA8ys6Y=
-82
View File
@@ -1,82 +0,0 @@
package api
import (
"encoding/json"
"net/http"
"github.com/gorilla/websocket"
"github.com/jwetzell/showbridge-go/internal/common"
)
var upgrader = websocket.Upgrader{
CheckOrigin: func(r *http.Request) bool {
return true
},
}
type WebsocketEventDestination struct {
conn *websocket.Conn
}
func (d WebsocketEventDestination) Send(event common.Event) error {
eventJSON, err := event.ToJSON()
if err != nil {
return err
}
return d.conn.WriteMessage(websocket.TextMessage, eventJSON)
}
func (d WebsocketEventDestination) Is(dest common.EventDestination) bool {
other, ok := dest.(WebsocketEventDestination)
if !ok {
return false
}
return d.conn == other.conn
}
func (as *ApiServer) handleWebsocket(w http.ResponseWriter, req *http.Request) {
conn, err := upgrader.Upgrade(w, req, nil)
if err != nil {
as.logger.Error("websocket upgrade error", "error", err)
return
}
defer conn.Close()
eventDestination := WebsocketEventDestination{conn: conn}
as.eventRouter.AddEventDestination(eventDestination)
READ_LOOP:
for {
messageType, message, err := conn.ReadMessage()
if err != nil {
_, ok := err.(*websocket.CloseError)
if ok {
break READ_LOOP
}
}
switch messageType {
case websocket.TextMessage, websocket.BinaryMessage:
event := common.Event{}
err = json.Unmarshal(message, &event)
if err != nil {
as.logger.Error("websocket message unmarshal error", "error", err)
continue
}
as.eventRouter.HandleEvent(event, WebsocketEventDestination{conn: conn})
case websocket.CloseMessage:
break READ_LOOP
case websocket.PingMessage:
err = conn.WriteMessage(websocket.PongMessage, nil)
if err != nil {
as.logger.Error("websocket pong error", "error", err)
}
default:
as.logger.Warn("unsupported websocket message type", "type", messageType)
continue
}
}
//NOTE(jwetzell): remove ws connection
as.eventRouter.RemoveEventDestination(eventDestination)
}
+2 -18
View File
@@ -81,19 +81,11 @@ func GetAnyAsByte(value any) (byte, bool) {
} }
func GetAnyAsByteSlice(value any) ([]byte, bool) { func GetAnyAsByteSlice(value any) ([]byte, bool) {
// already a []byte
byteSlice, ok := value.([]byte)
if ok {
return byteSlice, true
}
// check for a slice
v := reflect.ValueOf(value) v := reflect.ValueOf(value)
if v.Kind() != reflect.Slice && v.Kind() != reflect.Array { if v.Kind() != reflect.Slice {
return nil, false return nil, false
} }
// try to convert each element to a byte
result := make([]byte, v.Len()) result := make([]byte, v.Len())
for i := 0; i < v.Len(); i++ { for i := 0; i < v.Len(); i++ {
elem := v.Index(i).Interface() elem := v.Index(i).Interface()
@@ -107,16 +99,8 @@ func GetAnyAsByteSlice(value any) ([]byte, bool) {
} }
func GetAnyAsIntSlice(value any) ([]int, bool) { func GetAnyAsIntSlice(value any) ([]int, bool) {
// already a []int
intSlice, ok := value.([]int)
if ok {
return intSlice, true
}
// check for a slice
v := reflect.ValueOf(value) v := reflect.ValueOf(value)
if v.Kind() != reflect.Slice && v.Kind() != reflect.Array { if v.Kind() != reflect.Slice {
return nil, false return nil, false
} }
+8
View File
@@ -0,0 +1,8 @@
package common
type contextKey string
const RouterContextKey contextKey = contextKey("router")
const SourceContextKey contextKey = contextKey("source")
const ModulesContextKey contextKey = contextKey("modules")
const SenderContextKey contextKey = contextKey("sender")
-26
View File
@@ -1,26 +0,0 @@
package common
import (
"encoding/json"
)
type Event struct {
Type string `json:"type"`
Data any `json:"data,omitempty"`
Error string `json:"error,omitempty"`
}
func (e Event) ToJSON() ([]byte, error) {
return json.Marshal(e)
}
type EventDestination interface {
Send(event Event) error
Is(dest EventDestination) bool
}
type EventRouter interface {
HandleEvent(event Event, source EventDestination)
AddEventDestination(dest EventDestination)
RemoveEventDestination(dest EventDestination)
}
-24
View File
@@ -1,24 +0,0 @@
package common_test
import (
"testing"
"github.com/jwetzell/showbridge-go/internal/common"
)
func TestEventToJson(t *testing.T) {
e := common.Event{
Type: "test.event",
Data: map[string]any{
"key": "value",
},
}
jsonData, err := e.ToJSON()
if err != nil {
t.Fatalf("Failed to convert event to JSON: %v", err)
}
expectedJson := `{"type":"test.event","data":{"key":"value"}}`
if string(jsonData) != expectedJson {
t.Errorf("Expected JSON: %s, got: %s", expectedJson, string(jsonData))
}
}
+4 -8
View File
@@ -8,7 +8,7 @@ import (
type Module interface { type Module interface {
Id() string Id() string
Type() string Type() string
Start(context.Context, InputHandler) error Start(context.Context) error
Stop() Stop()
} }
@@ -17,14 +17,10 @@ type OutputModule interface {
} }
type KeyValueModule interface { type KeyValueModule interface {
Get(ctx context.Context, key string) (any, error) Get(key string) (any, error)
Set(ctx context.Context, key string, value any) error Set(key string, value any) error
} }
type DatabaseModule interface { type DatabaseModule interface {
QueryContext(ctx context.Context, query string, args ...any) (*sql.Rows, error) Database() *sql.DB
}
type PubSubModule interface {
Publish(ctx context.Context, topic string, payload any) error
} }
+36 -5
View File
@@ -1,9 +1,40 @@
package common package common
import (
"context"
)
type WrappedPayload struct { type WrappedPayload struct {
Payload any Payload any
InputHandler InputHandler Modules map[string]Module
Modules map[string]Module Sender any
Source string Source string
End bool End bool
}
func GetWrappedPayload(ctx context.Context, payload any) WrappedPayload {
wrappedPayload := WrappedPayload{
Payload: payload,
End: false,
}
modules := ctx.Value(ModulesContextKey)
if modules != nil {
moduleMap, ok := modules.(map[string]Module)
if ok {
wrappedPayload.Modules = moduleMap
} else {
wrappedPayload.Modules = make(map[string]Module)
}
}
sender := ctx.Value(SenderContextKey)
if sender != nil {
wrappedPayload.Sender = sender
}
source := ctx.Value(SourceContextKey)
if source != nil {
wrappedPayload.Source = source.(string)
}
return wrappedPayload
} }
+82
View File
@@ -0,0 +1,82 @@
package common_test
import (
"context"
"reflect"
"testing"
"github.com/jwetzell/showbridge-go/internal/common"
"github.com/jwetzell/showbridge-go/internal/test"
)
func TestGoodGetWrappedPayload(t *testing.T) {
testCases := []struct {
name string
ctx context.Context
payload any
expected common.WrappedPayload
}{
{
name: "basic",
ctx: t.Context(),
payload: "test",
expected: common.WrappedPayload{
Payload: "test",
},
},
{
name: "with modules in context",
ctx: test.GetContextWithModules(t.Context(), map[string]common.Module{}),
payload: "test",
expected: common.WrappedPayload{
Payload: "test",
Modules: map[string]common.Module{},
},
},
{
name: "with sender in context",
ctx: test.GetContextWithSender(t.Context(), "sender"),
payload: "test",
expected: common.WrappedPayload{
Payload: "test",
Sender: "sender",
},
},
{
name: "with source in context",
ctx: test.GetContextWithSource(t.Context(), "source"),
payload: "test",
expected: common.WrappedPayload{
Payload: "test",
Source: "source",
},
},
{
name: "with all fields in context",
ctx: test.GetContextWithSource(
test.GetContextWithSender(
test.GetContextWithModules(t.Context(), map[string]common.Module{}),
"sender",
),
"source",
),
payload: "test",
expected: common.WrappedPayload{
Payload: "test",
Modules: map[string]common.Module{},
Sender: "sender",
Source: "source",
},
},
}
for _, testCase := range testCases {
t.Run(testCase.name, func(t *testing.T) {
wrappedPayload := common.GetWrappedPayload(testCase.ctx, testCase.payload)
if !reflect.DeepEqual(wrappedPayload, testCase.expected) {
t.Fatalf("GetWrappedPayload expected got %+v, expected %+v", wrappedPayload, testCase.expected)
}
})
}
}
+7 -4
View File
@@ -1,12 +1,15 @@
package common package common
import ( import "context"
"context"
)
type InputHandler func(ctx context.Context, sourceId string, payload any) (bool, []RouteIOError) type RouteIO interface {
HandleInput(ctx context.Context, sourceId string, payload any) (bool, []RouteIOError)
HandleOutput(ctx context.Context, destinationId string, payload any) error
}
type RouteIOError struct { type RouteIOError struct {
Index int `json:"index"` Index int `json:"index"`
OutputError error `json:"outputError"`
ProcessError error `json:"processError"` ProcessError error `json:"processError"`
InputError error `json:"inputError"`
} }
-5
View File
@@ -5,8 +5,3 @@ type Config struct {
Modules []ModuleConfig `json:"modules"` Modules []ModuleConfig `json:"modules"`
Routes []RouteConfig `json:"routes"` Routes []RouteConfig `json:"routes"`
} }
type Configurable interface {
UpdateConfig(newConfig Config, triggerChangeChannel bool) ([]ModuleError, []RouteError, error)
GetRunningConfig() Config
}
-6
View File
@@ -5,9 +5,3 @@ type ModuleConfig struct {
Type string `json:"type"` Type string `json:"type"`
Params Params `json:"params,omitempty"` Params Params `json:"params,omitempty"`
} }
type ModuleError struct {
Index int `json:"index"`
Config ModuleConfig `json:"config"`
Error string `json:"error"`
}
-1
View File
@@ -1,7 +1,6 @@
package config package config
type ProcessorConfig struct { type ProcessorConfig struct {
Id string `json:"id"`
Type string `json:"type"` Type string `json:"type"`
Params Params `json:"params,omitempty"` Params Params `json:"params,omitempty"`
} }
-7
View File
@@ -1,13 +1,6 @@
package config package config
type RouteConfig struct { type RouteConfig struct {
Id string `json:"id"`
Input string `json:"input"` Input string `json:"input"`
Processors []ProcessorConfig `json:"processors"` Processors []ProcessorConfig `json:"processors"`
} }
type RouteError struct {
Index int `json:"index"`
Config RouteConfig `json:"config"`
Error string `json:"error"`
}
+5 -5
View File
@@ -10,15 +10,15 @@ type Framer interface {
func GetFramer(framingType string) Framer { func GetFramer(framingType string) Framer {
switch framingType { switch framingType {
case "CR": case "CR":
return newByteSeparatorFramer([]byte{'\r'}) return NewByteSeparatorFramer([]byte{'\r'})
case "LF": case "LF":
return newByteSeparatorFramer([]byte{'\n'}) return NewByteSeparatorFramer([]byte{'\n'})
case "CRLF": case "CRLF":
return newByteSeparatorFramer([]byte{'\r', '\n'}) return NewByteSeparatorFramer([]byte{'\r', '\n'})
case "SLIP": case "SLIP":
return newSlipFramer() return NewSlipFramer()
case "RAW": case "RAW":
return newRawFramer() return NewRawFramer()
default: default:
return nil return nil
} }
+7 -7
View File
@@ -1,23 +1,23 @@
package framer package framer
type rawFramer struct{} type RawFramer struct{}
func newRawFramer() *rawFramer { func NewRawFramer() *RawFramer {
return &rawFramer{} return &RawFramer{}
} }
func (rf *rawFramer) Decode(data []byte) [][]byte { func (rf *RawFramer) Decode(data []byte) [][]byte {
return [][]byte{data} return [][]byte{data}
} }
func (rf *rawFramer) Encode(data []byte) []byte { func (rf *RawFramer) Encode(data []byte) []byte {
return data return data
} }
func (rf *rawFramer) Clear() { func (rf *RawFramer) Clear() {
// NOTE(jwetzell): no internal state to clear // NOTE(jwetzell): no internal state to clear
} }
func (rf *rawFramer) Buffer() []byte { func (rf *RawFramer) Buffer() []byte {
return []byte{} return []byte{}
} }
+7 -7
View File
@@ -4,16 +4,16 @@ import (
"bytes" "bytes"
) )
type byteSeparatorFramer struct { type ByteSeparatorFramer struct {
buffer []byte buffer []byte
separator []byte separator []byte
} }
func newByteSeparatorFramer(separator []byte) *byteSeparatorFramer { func NewByteSeparatorFramer(separator []byte) *ByteSeparatorFramer {
return &byteSeparatorFramer{separator: separator, buffer: []byte{}} return &ByteSeparatorFramer{separator: separator, buffer: []byte{}}
} }
func (bsf *byteSeparatorFramer) Decode(data []byte) [][]byte { func (bsf *ByteSeparatorFramer) Decode(data []byte) [][]byte {
messages := [][]byte{} messages := [][]byte{}
bsf.buffer = append(bsf.buffer, data...) bsf.buffer = append(bsf.buffer, data...)
@@ -28,14 +28,14 @@ func (bsf *byteSeparatorFramer) Decode(data []byte) [][]byte {
return messages return messages
} }
func (bsf *byteSeparatorFramer) Encode(data []byte) []byte { func (bsf *ByteSeparatorFramer) Encode(data []byte) []byte {
return append(data, bsf.separator...) return append(data, bsf.separator...)
} }
func (bsf *byteSeparatorFramer) Clear() { func (bsf *ByteSeparatorFramer) Clear() {
bsf.buffer = []byte{} bsf.buffer = []byte{}
} }
func (bsf *byteSeparatorFramer) Buffer() []byte { func (bsf *ByteSeparatorFramer) Buffer() []byte {
return bsf.buffer return bsf.buffer
} }
+9 -10
View File
@@ -1,14 +1,14 @@
package framer package framer
type slipFramer struct { type SlipFramer struct {
buffer []byte buffer []byte
} }
func newSlipFramer() *slipFramer { func NewSlipFramer() *SlipFramer {
return &slipFramer{buffer: []byte{}} return &SlipFramer{buffer: []byte{}}
} }
func (sf *slipFramer) Decode(data []byte) [][]byte { func (sf *SlipFramer) Decode(data []byte) [][]byte {
messages := [][]byte{} messages := [][]byte{}
END := byte(0xc0) END := byte(0xc0)
@@ -25,10 +25,9 @@ func (sf *slipFramer) Decode(data []byte) [][]byte {
} }
if escapeNext { if escapeNext {
switch packetByte { if packetByte == ESC_END {
case ESC_END:
sf.buffer = append(sf.buffer, END) sf.buffer = append(sf.buffer, END)
case ESC_ESC: } else if packetByte == ESC_ESC {
sf.buffer = append(sf.buffer, ESC) sf.buffer = append(sf.buffer, ESC)
} }
escapeNext = false escapeNext = false
@@ -49,7 +48,7 @@ func (sf *slipFramer) Decode(data []byte) [][]byte {
return messages return messages
} }
func (sf *slipFramer) Encode(data []byte) []byte { func (sf *SlipFramer) Encode(data []byte) []byte {
END := byte(0xc0) END := byte(0xc0)
ESC := byte(0xdb) ESC := byte(0xdb)
ESC_END := byte(0xdc) ESC_END := byte(0xdc)
@@ -72,10 +71,10 @@ func (sf *slipFramer) Encode(data []byte) []byte {
return encodedBytes return encodedBytes
} }
func (sf *slipFramer) Clear() { func (sf *SlipFramer) Clear() {
sf.buffer = []byte{} sf.buffer = []byte{}
} }
func (sf *slipFramer) Buffer() []byte { func (sf *SlipFramer) Buffer() []byte {
return sf.buffer return sf.buffer
} }
+36 -48
View File
@@ -1,11 +1,13 @@
//go:build !js
package module package module
import ( import (
"context" "context"
"database/sql" "database/sql"
"errors"
"fmt" "fmt"
"log/slog" "log/slog"
"sync"
"github.com/google/jsonschema-go/jsonschema" "github.com/google/jsonschema-go/jsonschema"
"github.com/jwetzell/showbridge-go/internal/common" "github.com/jwetzell/showbridge-go/internal/common"
@@ -14,6 +16,15 @@ import (
_ "modernc.org/sqlite" _ "modernc.org/sqlite"
) )
type DbSqlite struct {
config config.ModuleConfig
Dsn string
ctx context.Context
router common.RouteIO
db *sql.DB
logger *slog.Logger
}
func init() { func init() {
RegisterModule(ModuleRegistration{ RegisterModule(ModuleRegistration{
Type: "db.sqlite", Type: "db.sqlite",
@@ -22,14 +33,11 @@ func init() {
Type: "object", Type: "object",
Properties: map[string]*jsonschema.Schema{ Properties: map[string]*jsonschema.Schema{
"dsn": { "dsn": {
Title: "Data Source Name", Type: "string",
Description: "the data source name (DSN) for the SQLite database", MinLength: jsonschema.Ptr(1),
Type: "string",
MinLength: new(1),
}, },
}, },
Required: []string{"dsn"}, Required: []string{"dsn"},
AdditionalProperties: &jsonschema.Schema{Not: &jsonschema.Schema{}},
}, },
New: func(config config.ModuleConfig) (common.Module, error) { New: func(config config.ModuleConfig) (common.Module, error) {
params := config.Params params := config.Params
@@ -44,60 +52,40 @@ func init() {
}) })
} }
type DbSqlite struct { func (t *DbSqlite) Id() string {
config config.ModuleConfig return t.config.Id
Dsn string
ctx context.Context
inputHandler common.InputHandler
db *sql.DB
logger *slog.Logger
dbMu sync.Mutex
cancel context.CancelFunc
} }
func (dbs *DbSqlite) Id() string { func (t *DbSqlite) Type() string {
return dbs.config.Id return t.config.Type
} }
func (dbs *DbSqlite) Type() string { func (t *DbSqlite) Start(ctx context.Context) error {
return dbs.config.Type t.logger.Debug("running")
} router, ok := ctx.Value(common.RouterContextKey).(common.RouteIO)
func (dbs *DbSqlite) Start(ctx context.Context, inputHandler common.InputHandler) error { if !ok {
dbs.logger.Debug("running") return errors.New("db.sqlite unable to get router from context")
dbs.inputHandler = inputHandler }
moduleContext, cancel := context.WithCancel(ctx) t.router = router
dbs.ctx = moduleContext t.ctx = ctx
dbs.cancel = cancel
db, err := sql.Open("sqlite", dbs.Dsn) db, err := sql.Open("sqlite", t.Dsn)
if err != nil { if err != nil {
return fmt.Errorf("db.sqlite error opening database: %w", err) return fmt.Errorf("db.sqlite error opening database: %w", err)
} }
dbs.dbMu.Lock() t.db = db
dbs.db = db defer t.db.Close()
dbs.dbMu.Unlock() <-ctx.Done()
<-dbs.ctx.Done()
dbs.logger.Debug("done")
return nil return nil
} }
func (dbs *DbSqlite) Stop() { func (t *DbSqlite) Stop() {
if dbs.cancel != nil { if t.db != nil {
defer dbs.cancel() t.db.Close()
}
dbs.dbMu.Lock()
defer dbs.dbMu.Unlock()
if dbs.db != nil {
dbs.db.Close()
} }
} }
func (dbs *DbSqlite) QueryContext(ctx context.Context, query string, args ...any) (*sql.Rows, error) { func (t *DbSqlite) Database() *sql.DB {
dbs.dbMu.Lock() return t.db
defer dbs.dbMu.Unlock()
if dbs.db == nil {
return nil, fmt.Errorf("database not initialized")
}
return dbs.db.QueryContext(ctx, query, args...)
} }
+7
View File
@@ -0,0 +1,7 @@
//go:build cgo
package module
import (
_ "gitlab.com/gomidi/midi/v2/drivers/rtmididrv"
)
+70 -61
View File
@@ -1,3 +1,5 @@
//go:build !js
package module package module
import ( import (
@@ -6,9 +8,8 @@ import (
"errors" "errors"
"fmt" "fmt"
"log/slog" "log/slog"
"net"
"net/http" "net/http"
"sync"
"time"
"github.com/google/jsonschema-go/jsonschema" "github.com/google/jsonschema-go/jsonschema"
"github.com/jwetzell/showbridge-go/internal/common" "github.com/jwetzell/showbridge-go/internal/common"
@@ -16,49 +17,20 @@ import (
"github.com/jwetzell/showbridge-go/internal/processor" "github.com/jwetzell/showbridge-go/internal/processor"
) )
func init() {
RegisterModule(ModuleRegistration{
Type: "http.server",
Title: "HTTP Server",
ParamsSchema: &jsonschema.Schema{
Type: "object",
Properties: map[string]*jsonschema.Schema{
"port": {
Title: "Port",
Description: "the port for the HTTP server to listen on",
Type: "integer",
Minimum: jsonschema.Ptr[float64](1024),
Maximum: jsonschema.Ptr[float64](65535),
},
},
Required: []string{"port"},
AdditionalProperties: &jsonschema.Schema{Not: &jsonschema.Schema{}},
},
New: func(config config.ModuleConfig) (common.Module, error) {
params := config.Params
portNum, err := params.GetInt("port")
if err != nil {
return nil, fmt.Errorf("http.server port error: %w", err)
}
return &HTTPServer{Port: uint16(portNum), config: config, logger: CreateLogger(config)}, nil
},
})
}
type HTTPServer struct { type HTTPServer struct {
config config.ModuleConfig config config.ModuleConfig
Port uint16 Port uint16
ctx context.Context ctx context.Context
inputHandler common.InputHandler router common.RouteIO
logger *slog.Logger logger *slog.Logger
cancel context.CancelFunc cancel context.CancelFunc
server *http.Server
serverMu sync.Mutex
} }
type ResponseIOError struct { type ResponseIOError struct {
Index int `json:"index"` Index int `json:"index"`
OutputError *string `json:"outputError"`
ProcessError *string `json:"processError"` ProcessError *string `json:"processError"`
InputError *string `json:"inputError"`
} }
type IOResponseData struct { type IOResponseData struct {
@@ -84,6 +56,34 @@ func (hsrw *HTTPServerResponseWriter) Write(data []byte) (int, error) {
return hsrw.ResponseWriter.Write(data) return hsrw.ResponseWriter.Write(data)
} }
func init() {
RegisterModule(ModuleRegistration{
Type: "http.server",
Title: "HTTP Server",
ParamsSchema: &jsonschema.Schema{
Type: "object",
Properties: map[string]*jsonschema.Schema{
"port": {
Title: "Port",
Type: "integer",
Minimum: jsonschema.Ptr[float64](1024),
Maximum: jsonschema.Ptr[float64](65535),
},
},
Required: []string{"port"},
AdditionalProperties: nil,
},
New: func(config config.ModuleConfig) (common.Module, error) {
params := config.Params
portNum, err := params.GetInt("port")
if err != nil {
return nil, fmt.Errorf("http.server port error: %w", err)
}
return &HTTPServer{Port: uint16(portNum), config: config, logger: CreateLogger(config)}, nil
},
})
}
func (hs *HTTPServer) Id() string { func (hs *HTTPServer) Id() string {
return hs.config.Id return hs.config.Id
} }
@@ -99,9 +99,13 @@ func (hs *HTTPServer) ServeHTTP(w http.ResponseWriter, r *http.Request) {
Message: "routing successful", Message: "routing successful",
Status: "ok", Status: "ok",
} }
if hs.inputHandler != nil { if hs.router != nil {
inputContext := context.WithValue(hs.ctx, httpServerContextKey("responseWriter"), &responseWriter) inputContext := context.WithValue(hs.ctx, httpServerContextKey("responseWriter"), &responseWriter)
aRouteFound, routingErrors := hs.inputHandler(inputContext, hs.Id(), r) senderAddr, err := net.ResolveTCPAddr("tcp", r.RemoteAddr)
if err == nil {
inputContext = context.WithValue(inputContext, common.SenderContextKey, senderAddr)
}
aRouteFound, routingErrors := hs.router.HandleInput(inputContext, hs.Id(), r)
if !responseWriter.done { if !responseWriter.done {
if aRouteFound { if aRouteFound {
if routingErrors != nil { if routingErrors != nil {
@@ -115,11 +119,21 @@ func (hs *HTTPServer) ServeHTTP(w http.ResponseWriter, r *http.Request) {
Index: responseIOError.Index, Index: responseIOError.Index,
} }
if responseIOError.InputError != nil {
errorMsg := responseIOError.InputError.Error()
errorToAdd.InputError = &errorMsg
}
if responseIOError.ProcessError != nil { if responseIOError.ProcessError != nil {
errorMsg := responseIOError.ProcessError.Error() errorMsg := responseIOError.ProcessError.Error()
errorToAdd.ProcessError = &errorMsg errorToAdd.ProcessError = &errorMsg
} }
if responseIOError.OutputError != nil {
errorMsg := responseIOError.OutputError.Error()
errorToAdd.OutputError = &errorMsg
}
response.IOErrors = append(response.IOErrors, errorToAdd) response.IOErrors = append(response.IOErrors, errorToAdd)
} }
@@ -148,27 +162,32 @@ func (hs *HTTPServer) ServeHTTP(w http.ResponseWriter, r *http.Request) {
} }
} }
func (hs *HTTPServer) Start(ctx context.Context, inputHandler common.InputHandler) error { func (hs *HTTPServer) Start(ctx context.Context) error {
hs.logger.Debug("running") hs.logger.Debug("running")
hs.inputHandler = inputHandler router, ok := ctx.Value(common.RouterContextKey).(common.RouteIO)
if !ok {
return errors.New("http.server unable to get router from context")
}
hs.router = router
moduleContext, cancel := context.WithCancel(ctx) moduleContext, cancel := context.WithCancel(ctx)
hs.ctx = moduleContext hs.ctx = moduleContext
hs.cancel = cancel hs.cancel = cancel
httpServer := &http.Server{ httpServer := &http.Server{
Addr: fmt.Sprintf(":%d", hs.Port), Addr: fmt.Sprintf(":%d", hs.Port),
ReadHeaderTimeout: 5 * time.Second, Handler: hs,
Handler: hs,
} }
hs.serverMu.Lock() go func() {
hs.server = httpServer <-hs.ctx.Done()
hs.serverMu.Unlock() httpServer.Close()
}()
err := httpServer.ListenAndServe() err := httpServer.ListenAndServe()
// TODO(jwetzell): handle server closed error differently // TODO(jwetzell): handle server closed error differently
if err != nil { if err != nil {
if !errors.Is(err, http.ErrServerClosed) { if err.Error() != "http: Server closed" {
return err return err
} }
} }
@@ -201,15 +220,5 @@ func (hs *HTTPServer) Output(ctx context.Context, payload any) error {
} }
func (hs *HTTPServer) Stop() { func (hs *HTTPServer) Stop() {
if hs.cancel != nil { hs.cancel()
defer hs.cancel()
}
hs.serverMu.Lock()
defer hs.serverMu.Unlock()
if hs.server != nil {
shutdownCtx, shutdownCancel := context.WithTimeout(context.Background(), 5*time.Second)
hs.server.Shutdown(shutdownCtx)
shutdownCancel()
<-shutdownCtx.Done()
}
} }
+28 -28
View File
@@ -1,9 +1,10 @@
//go:build cgo //go:build cgo || js
package module package module
import ( import (
"context" "context"
"errors"
"fmt" "fmt"
"log/slog" "log/slog"
@@ -11,9 +12,18 @@ import (
"github.com/jwetzell/showbridge-go/internal/common" "github.com/jwetzell/showbridge-go/internal/common"
"github.com/jwetzell/showbridge-go/internal/config" "github.com/jwetzell/showbridge-go/internal/config"
"gitlab.com/gomidi/midi/v2" "gitlab.com/gomidi/midi/v2"
_ "gitlab.com/gomidi/midi/v2/drivers/rtmididrv"
) )
type MIDIInput struct {
config config.ModuleConfig
ctx context.Context
router common.RouteIO
Port string
SendFunc func(midi.Message) error
logger *slog.Logger
cancel context.CancelFunc
}
func init() { func init() {
RegisterModule(ModuleRegistration{ RegisterModule(ModuleRegistration{
Type: "midi.input", Type: "midi.input",
@@ -22,13 +32,12 @@ func init() {
Type: "object", Type: "object",
Properties: map[string]*jsonschema.Schema{ Properties: map[string]*jsonschema.Schema{
"port": { "port": {
Title: "Port", Title: "Port",
Description: "the name of the MIDI port to listen to", Type: "string",
Type: "string",
}, },
}, },
Required: []string{"port"}, Required: []string{"port"},
AdditionalProperties: &jsonschema.Schema{Not: &jsonschema.Schema{}}, AdditionalProperties: nil,
}, },
New: func(config config.ModuleConfig) (common.Module, error) { New: func(config config.ModuleConfig) (common.Module, error) {
params := config.Params params := config.Params
@@ -42,16 +51,6 @@ func init() {
}) })
} }
type MIDIInput struct {
config config.ModuleConfig
ctx context.Context
inputHandler common.InputHandler
Port string
logger *slog.Logger
cancel context.CancelFunc
stop func()
}
func (mi *MIDIInput) Id() string { func (mi *MIDIInput) Id() string {
return mi.config.Id return mi.config.Id
} }
@@ -60,9 +59,15 @@ func (mi *MIDIInput) Type() string {
return mi.config.Type return mi.config.Type
} }
func (mi *MIDIInput) Start(ctx context.Context, inputHandler common.InputHandler) error { func (mi *MIDIInput) Start(ctx context.Context) error {
mi.logger.Debug("running") mi.logger.Debug("running")
mi.inputHandler = inputHandler defer midi.CloseDriver()
router, ok := ctx.Value(common.RouterContextKey).(common.RouteIO)
if !ok {
return errors.New("midi.input unable to get router from context")
}
mi.router = router
moduleContext, cancel := context.WithCancel(ctx) moduleContext, cancel := context.WithCancel(ctx)
mi.ctx = moduleContext mi.ctx = moduleContext
mi.cancel = cancel mi.cancel = cancel
@@ -73,15 +78,16 @@ func (mi *MIDIInput) Start(ctx context.Context, inputHandler common.InputHandler
} }
stop, err := midi.ListenTo(in, func(msg midi.Message, timestampms int32) { stop, err := midi.ListenTo(in, func(msg midi.Message, timestampms int32) {
if mi.inputHandler != nil { if mi.router != nil {
mi.inputHandler(mi.ctx, mi.Id(), msg) mi.router.HandleInput(mi.ctx, mi.Id(), msg)
} }
}, midi.UseSysEx()) }, midi.UseSysEx())
if err != nil { if err != nil {
return err return err
} }
mi.stop = stop
defer stop()
<-mi.ctx.Done() <-mi.ctx.Done()
mi.logger.Debug("done") mi.logger.Debug("done")
@@ -89,11 +95,5 @@ func (mi *MIDIInput) Start(ctx context.Context, inputHandler common.InputHandler
} }
func (mi *MIDIInput) Stop() { func (mi *MIDIInput) Stop() {
if mi.cancel != nil { mi.cancel()
defer mi.cancel()
}
if mi.stop != nil {
mi.stop()
}
midi.CloseDriver()
} }
+26 -31
View File
@@ -1,4 +1,4 @@
//go:build cgo //go:build cgo || js
package module package module
@@ -7,15 +7,23 @@ import (
"errors" "errors"
"fmt" "fmt"
"log/slog" "log/slog"
"sync"
"github.com/google/jsonschema-go/jsonschema" "github.com/google/jsonschema-go/jsonschema"
"github.com/jwetzell/showbridge-go/internal/common" "github.com/jwetzell/showbridge-go/internal/common"
"github.com/jwetzell/showbridge-go/internal/config" "github.com/jwetzell/showbridge-go/internal/config"
"gitlab.com/gomidi/midi/v2" "gitlab.com/gomidi/midi/v2"
_ "gitlab.com/gomidi/midi/v2/drivers/rtmididrv"
) )
type MIDIOutput struct {
config config.ModuleConfig
ctx context.Context
router common.RouteIO
Port string
SendFunc func(midi.Message) error
logger *slog.Logger
cancel context.CancelFunc
}
func init() { func init() {
RegisterModule(ModuleRegistration{ RegisterModule(ModuleRegistration{
Type: "midi.output", Type: "midi.output",
@@ -24,13 +32,12 @@ func init() {
Type: "object", Type: "object",
Properties: map[string]*jsonschema.Schema{ Properties: map[string]*jsonschema.Schema{
"port": { "port": {
Title: "Port", Title: "Port",
Description: "the name of the MIDI port to send messages to", Type: "string",
Type: "string",
}, },
}, },
Required: []string{"port"}, Required: []string{"port"},
AdditionalProperties: &jsonschema.Schema{Not: &jsonschema.Schema{}}, AdditionalProperties: nil,
}, },
New: func(config config.ModuleConfig) (common.Module, error) { New: func(config config.ModuleConfig) (common.Module, error) {
params := config.Params params := config.Params
@@ -45,17 +52,6 @@ func init() {
}) })
} }
type MIDIOutput struct {
config config.ModuleConfig
ctx context.Context
inputHandler common.InputHandler
Port string
sendFunc func(midi.Message) error
logger *slog.Logger
cancel context.CancelFunc
sendFuncMu sync.Mutex
}
func (mo *MIDIOutput) Id() string { func (mo *MIDIOutput) Id() string {
return mo.config.Id return mo.config.Id
} }
@@ -64,9 +60,15 @@ func (mo *MIDIOutput) Type() string {
return mo.config.Type return mo.config.Type
} }
func (mo *MIDIOutput) Start(ctx context.Context, inputHandler common.InputHandler) error { func (mo *MIDIOutput) Start(ctx context.Context) error {
mo.logger.Debug("running") mo.logger.Debug("running")
mo.inputHandler = inputHandler defer midi.CloseDriver()
router, ok := ctx.Value(common.RouterContextKey).(common.RouteIO)
if !ok {
return errors.New("midi.output unable to get router from context")
}
mo.router = router
moduleContext, cancel := context.WithCancel(ctx) moduleContext, cancel := context.WithCancel(ctx)
mo.ctx = moduleContext mo.ctx = moduleContext
mo.cancel = cancel mo.cancel = cancel
@@ -82,9 +84,7 @@ func (mo *MIDIOutput) Start(ctx context.Context, inputHandler common.InputHandle
return err return err
} }
mo.sendFuncMu.Lock() mo.SendFunc = send
mo.sendFunc = send
mo.sendFuncMu.Unlock()
<-mo.ctx.Done() <-mo.ctx.Done()
mo.logger.Debug("done") mo.logger.Debug("done")
@@ -92,9 +92,7 @@ func (mo *MIDIOutput) Start(ctx context.Context, inputHandler common.InputHandle
} }
func (mo *MIDIOutput) Output(ctx context.Context, payload any) error { func (mo *MIDIOutput) Output(ctx context.Context, payload any) error {
mo.sendFuncMu.Lock() if mo.SendFunc == nil {
defer mo.sendFuncMu.Unlock()
if mo.sendFunc == nil {
return errors.New("midi.output output is not setup") return errors.New("midi.output output is not setup")
} }
@@ -104,12 +102,9 @@ func (mo *MIDIOutput) Output(ctx context.Context, payload any) error {
return errors.New("midi.output can only output midi.Message") return errors.New("midi.output can only output midi.Message")
} }
return mo.sendFunc(payloadMessage) return mo.SendFunc(payloadMessage)
} }
func (mo *MIDIOutput) Stop() { func (mo *MIDIOutput) Stop() {
if mo.cancel != nil { mo.cancel()
defer mo.cancel()
}
midi.CloseDriver()
} }
+9 -25
View File
@@ -10,6 +10,12 @@ import (
"github.com/jwetzell/showbridge-go/internal/config" "github.com/jwetzell/showbridge-go/internal/config"
) )
type ModuleError struct {
Index int `json:"index"`
Config config.ModuleConfig `json:"config"`
Error string `json:"error"`
}
type ModuleRegistration struct { type ModuleRegistration struct {
Type string `json:"type"` Type string `json:"type"`
Title string `json:"title,omitempty"` Title string `json:"title,omitempty"`
@@ -30,37 +36,15 @@ func RegisterModule(mod ModuleRegistration) {
moduleRegistryMu.Lock() moduleRegistryMu.Lock()
defer moduleRegistryMu.Unlock() defer moduleRegistryMu.Unlock()
_, exists := moduleRegistry[string(mod.Type)] if _, ok := ModuleRegistry[string(mod.Type)]; ok {
if exists {
panic(fmt.Sprintf("module already registered: %s", mod.Type)) panic(fmt.Sprintf("module already registered: %s", mod.Type))
} }
moduleRegistry[string(mod.Type)] = mod ModuleRegistry[string(mod.Type)] = mod
}
type ModuleRegistry map[string]ModuleRegistration
func GetModuleRegistration(moduleType string) (ModuleRegistration, bool) {
moduleRegistryMu.RLock()
defer moduleRegistryMu.RUnlock()
mod, ok := moduleRegistry[moduleType]
return mod, ok
}
func GetModuleRegistrations() []ModuleRegistration {
moduleRegistryMu.RLock()
defer moduleRegistryMu.RUnlock()
registrations := make([]ModuleRegistration, 0, len(moduleRegistry))
for _, mod := range moduleRegistry {
registrations = append(registrations, mod)
}
return registrations
} }
var ( var (
moduleRegistryMu sync.RWMutex moduleRegistryMu sync.RWMutex
moduleRegistry = make(ModuleRegistry) ModuleRegistry = make(map[string]ModuleRegistration)
) )
func CreateLogger(config config.ModuleConfig) *slog.Logger { func CreateLogger(config config.ModuleConfig) *slog.Logger {
+38 -87
View File
@@ -1,12 +1,12 @@
//go:build !js
package module package module
import ( import (
"context" "context"
"encoding/json"
"errors" "errors"
"fmt" "fmt"
"log/slog" "log/slog"
"sync"
mqtt "github.com/eclipse/paho.mqtt.golang" mqtt "github.com/eclipse/paho.mqtt.golang"
"github.com/google/jsonschema-go/jsonschema" "github.com/google/jsonschema-go/jsonschema"
@@ -14,6 +14,18 @@ import (
"github.com/jwetzell/showbridge-go/internal/config" "github.com/jwetzell/showbridge-go/internal/config"
) )
type MQTTClient struct {
config config.ModuleConfig
ctx context.Context
router common.RouteIO
Broker string
ClientID string
Topic string
client mqtt.Client
logger *slog.Logger
cancel context.CancelFunc
}
func init() { func init() {
RegisterModule(ModuleRegistration{ RegisterModule(ModuleRegistration{
Type: "mqtt.client", Type: "mqtt.client",
@@ -22,40 +34,23 @@ func init() {
Type: "object", Type: "object",
Properties: map[string]*jsonschema.Schema{ Properties: map[string]*jsonschema.Schema{
"broker": { "broker": {
Title: "Broker URL", Title: "Broker URL",
Description: "the URL of the MQTT broker to connect to", Type: "string",
Type: "string",
}, },
"topic": { "topic": {
Title: "Topic", Title: "Topic",
Description: "an MQTT topic to subscribe to", Type: "string",
Type: "string",
}, },
"clientId": { "clientId": {
Title: "Client ID", Title: "Client ID",
Description: "the client ID to use when connecting to the MQTT broker", Type: "string",
Type: "string",
},
"qos": {
Title: "QoS",
Description: "the QoS level to use when publishing messages",
Type: "integer",
Minimum: jsonschema.Ptr[float64](0),
Maximum: jsonschema.Ptr[float64](2),
Default: json.RawMessage(`0`),
},
"retained": {
Title: "Retained",
Description: "whether to set the retained flag when publishing messages",
Type: "boolean",
Default: json.RawMessage(`false`),
}, },
}, },
Required: []string{"broker", "topic", "clientId"}, Required: []string{"broker", "topic", "clientId"},
AdditionalProperties: &jsonschema.Schema{Not: &jsonschema.Schema{}}, AdditionalProperties: nil,
}, },
New: func(moduleConfig config.ModuleConfig) (common.Module, error) { New: func(config config.ModuleConfig) (common.Module, error) {
params := moduleConfig.Params params := config.Params
brokerString, err := params.GetString("broker") brokerString, err := params.GetString("broker")
if err != nil { if err != nil {
@@ -74,46 +69,11 @@ func init() {
return nil, fmt.Errorf("mqtt.client clientId error: %w", err) return nil, fmt.Errorf("mqtt.client clientId error: %w", err)
} }
qosString, err := params.GetInt("qos") return &MQTTClient{config: config, Broker: brokerString, Topic: topicString, ClientID: clientIdString, logger: CreateLogger(config)}, nil
if err != nil {
if errors.Is(err, config.ErrParamNotFound) {
qosString = 0
} else {
return nil, fmt.Errorf("mqtt.client qos error: %w", err)
}
}
retainedBool, err := params.GetBool("retained")
if err != nil {
if errors.Is(err, config.ErrParamNotFound) {
retainedBool = false
} else {
return nil, fmt.Errorf("mqtt.client retained error: %w", err)
}
}
return &MQTTClient{config: moduleConfig, Broker: brokerString, Topic: topicString, ClientID: clientIdString, QoS: byte(qosString), Retained: retainedBool, logger: CreateLogger(moduleConfig)}, nil
}, },
}) })
} }
type MQTTClient struct {
config config.ModuleConfig
ctx context.Context
inputHandler common.InputHandler
Broker string
ClientID string
Topic string
QoS byte
Retained bool
client mqtt.Client
logger *slog.Logger
cancel context.CancelFunc
clientMu sync.Mutex
}
func (mc *MQTTClient) Id() string { func (mc *MQTTClient) Id() string {
return mc.config.Id return mc.config.Id
} }
@@ -122,9 +82,14 @@ func (mc *MQTTClient) Type() string {
return mc.config.Type return mc.config.Type
} }
func (mc *MQTTClient) Start(ctx context.Context, inputHandler common.InputHandler) error { func (mc *MQTTClient) Start(ctx context.Context) error {
mc.logger.Debug("running") mc.logger.Debug("running")
mc.inputHandler = inputHandler router, ok := ctx.Value(common.RouterContextKey).(common.RouteIO)
if !ok {
return errors.New("mqtt.client unable to get router from context")
}
mc.router = router
moduleContext, cancel := context.WithCancel(ctx) moduleContext, cancel := context.WithCancel(ctx)
mc.ctx = moduleContext mc.ctx = moduleContext
mc.cancel = cancel mc.cancel = cancel
@@ -137,18 +102,15 @@ func (mc *MQTTClient) Start(ctx context.Context, inputHandler common.InputHandle
opts.OnConnect = func(c mqtt.Client) { opts.OnConnect = func(c mqtt.Client) {
token := mc.client.Subscribe(mc.Topic, 1, func(c mqtt.Client, m mqtt.Message) { token := mc.client.Subscribe(mc.Topic, 1, func(c mqtt.Client, m mqtt.Message) {
if mc.inputHandler != nil { mc.router.HandleInput(mc.ctx, mc.Id(), m)
mc.inputHandler(mc.ctx, mc.Id(), m)
}
}) })
token.Wait() token.Wait()
} }
mc.clientMu.Lock()
mc.client = mqtt.NewClient(opts) mc.client = mqtt.NewClient(opts)
defer mc.client.Disconnect(250)
token := mc.client.Connect() token := mc.client.Connect()
mc.clientMu.Unlock()
token.Wait() token.Wait()
err := token.Error() err := token.Error()
@@ -161,15 +123,11 @@ func (mc *MQTTClient) Start(ctx context.Context, inputHandler common.InputHandle
return nil return nil
} }
func (mc *MQTTClient) Publish(ctx context.Context, topic string, payload any) error { func (mc *MQTTClient) Output(ctx context.Context, payload any) error {
payloadBytes, ok := common.GetAnyAsByteSlice(payload) payloadMessage, ok := common.GetAnyAs[mqtt.Message](payload)
if !ok { if !ok {
payloadString, ok := common.GetAnyAs[string](payload) return errors.New("mqtt.client is only able to output a MQTTMessage")
if !ok {
return errors.New("mqtt.client is only able to publish bytes or string")
}
payloadBytes = []byte(payloadString)
} }
if mc.client == nil { if mc.client == nil {
@@ -180,7 +138,7 @@ func (mc *MQTTClient) Publish(ctx context.Context, topic string, payload any) er
return errors.New("mqtt.client is not connected") return errors.New("mqtt.client is not connected")
} }
token := mc.client.Publish(topic, mc.QoS, mc.Retained, payloadBytes) token := mc.client.Publish(payloadMessage.Topic(), payloadMessage.Qos(), payloadMessage.Retained(), payloadMessage.Payload())
token.Wait() token.Wait()
@@ -188,12 +146,5 @@ func (mc *MQTTClient) Publish(ctx context.Context, topic string, payload any) er
} }
func (mc *MQTTClient) Stop() { func (mc *MQTTClient) Stop() {
if mc.cancel != nil { mc.cancel()
mc.cancel()
}
mc.clientMu.Lock()
defer mc.clientMu.Unlock()
if mc.client != nil {
mc.client.Disconnect(250)
}
} }
+39 -58
View File
@@ -1,17 +1,30 @@
//go:build !js
package module package module
import ( import (
"context" "context"
"errors" "errors"
"log/slog" "log/slog"
"sync"
"github.com/google/jsonschema-go/jsonschema" "github.com/google/jsonschema-go/jsonschema"
"github.com/jwetzell/showbridge-go/internal/common" "github.com/jwetzell/showbridge-go/internal/common"
"github.com/jwetzell/showbridge-go/internal/config" "github.com/jwetzell/showbridge-go/internal/config"
"github.com/jwetzell/showbridge-go/internal/processor"
"github.com/nats-io/nats.go" "github.com/nats-io/nats.go"
) )
type NATSClient struct {
config config.ModuleConfig
ctx context.Context
router common.RouteIO
URL string
Subject string
client *nats.Conn
logger *slog.Logger
cancel context.CancelFunc
}
func init() { func init() {
RegisterModule(ModuleRegistration{ RegisterModule(ModuleRegistration{
Type: "nats.client", Type: "nats.client",
@@ -20,18 +33,16 @@ func init() {
Type: "object", Type: "object",
Properties: map[string]*jsonschema.Schema{ Properties: map[string]*jsonschema.Schema{
"url": { "url": {
Title: "NATS Server URL", Title: "NATS Server URL",
Description: "the URL of the NATS server to connect to", Type: "string",
Type: "string",
}, },
"subject": { "subject": {
Title: "Subject", Title: "Subject",
Description: "the subject to subscribe to", Type: "string",
Type: "string",
}, },
}, },
Required: []string{"url", "subject"}, Required: []string{"url", "subject"},
AdditionalProperties: &jsonschema.Schema{Not: &jsonschema.Schema{}}, AdditionalProperties: nil,
}, },
New: func(config config.ModuleConfig) (common.Module, error) { New: func(config config.ModuleConfig) (common.Module, error) {
params := config.Params params := config.Params
@@ -51,20 +62,6 @@ func init() {
}) })
} }
type NATSClient struct {
config config.ModuleConfig
ctx context.Context
inputHandler common.InputHandler
URL string
Subject string
client *nats.Conn
logger *slog.Logger
cancel context.CancelFunc
sub *nats.Subscription
subMu sync.Mutex
clientMu sync.Mutex
}
func (nc *NATSClient) Id() string { func (nc *NATSClient) Id() string {
return nc.config.Id return nc.config.Id
} }
@@ -73,9 +70,15 @@ func (nc *NATSClient) Type() string {
return nc.config.Type return nc.config.Type
} }
func (nc *NATSClient) Start(ctx context.Context, inputHandler common.InputHandler) error { func (nc *NATSClient) Start(ctx context.Context) error {
nc.logger.Debug("running") nc.logger.Debug("running")
nc.inputHandler = inputHandler router, ok := ctx.Value(common.RouterContextKey).(common.RouteIO)
if !ok {
return errors.New("nats.client unable to get router from context")
}
nc.router = router
moduleContext, cancel := context.WithCancel(ctx) moduleContext, cancel := context.WithCancel(ctx)
nc.ctx = moduleContext nc.ctx = moduleContext
nc.cancel = cancel nc.cancel = cancel
@@ -86,43 +89,36 @@ func (nc *NATSClient) Start(ctx context.Context, inputHandler common.InputHandle
return err return err
} }
nc.clientMu.Lock()
nc.client = client nc.client = client
nc.clientMu.Unlock()
defer client.Drain()
defer client.Close()
sub, err := nc.client.Subscribe(nc.Subject, func(msg *nats.Msg) { sub, err := nc.client.Subscribe(nc.Subject, func(msg *nats.Msg) {
if nc.inputHandler != nil { if nc.router != nil {
nc.inputHandler(nc.ctx, nc.Id(), msg) nc.router.HandleInput(nc.ctx, nc.Id(), msg)
} }
}) })
if err != nil { if err != nil {
return err return err
} }
nc.subMu.Lock()
nc.sub = sub defer sub.Unsubscribe()
nc.subMu.Unlock()
<-nc.ctx.Done() <-nc.ctx.Done()
nc.logger.Debug("done") nc.logger.Debug("done")
return nil return nil
} }
func (nc *NATSClient) Publish(ctx context.Context, topic string, payload any) error { func (nc *NATSClient) Output(ctx context.Context, payload any) error {
payloadBytes, ok := common.GetAnyAsByteSlice(payload) payloadMessage, ok := common.GetAnyAs[processor.NATSMessage](payload)
if !ok { if !ok {
payloadString, ok := common.GetAnyAs[string](payload) return errors.New("nats.client is only able to output NATSMessage")
if !ok {
return errors.New("nats.client is only able to publish bytes or string")
}
payloadBytes = []byte(payloadString)
} }
nc.clientMu.Lock()
defer nc.clientMu.Unlock()
if nc.client == nil { if nc.client == nil {
return errors.New("nats.client client is not setup") return errors.New("nats.client client is not setup")
} }
@@ -131,26 +127,11 @@ func (nc *NATSClient) Publish(ctx context.Context, topic string, payload any) er
return errors.New("nats.client is not connected") return errors.New("nats.client is not connected")
} }
err := nc.client.Publish(topic, payloadBytes) err := nc.client.Publish(payloadMessage.Subject, payloadMessage.Payload)
return err return err
} }
func (nc *NATSClient) Stop() { func (nc *NATSClient) Stop() {
if nc.cancel != nil { nc.cancel()
defer nc.cancel()
}
nc.subMu.Lock()
defer nc.subMu.Unlock()
if nc.sub != nil {
nc.sub.Unsubscribe()
}
nc.clientMu.Lock()
defer nc.clientMu.Unlock()
if nc.client != nil {
nc.client.Drain()
// TODO(jwetzell): setup closed callback to get when client is fully closed
nc.client.Close()
}
} }
+39 -39
View File
@@ -1,3 +1,5 @@
//go:build !js
package module package module
import ( import (
@@ -7,7 +9,6 @@ import (
"fmt" "fmt"
"log/slog" "log/slog"
"net" "net"
"sync"
"time" "time"
"github.com/google/jsonschema-go/jsonschema" "github.com/google/jsonschema-go/jsonschema"
@@ -16,6 +17,17 @@ import (
"github.com/nats-io/nats-server/v2/server" "github.com/nats-io/nats-server/v2/server"
) )
type NATSServer struct {
config config.ModuleConfig
ctx context.Context
Ip string
Port int
router common.RouteIO
server *server.Server
logger *slog.Logger
cancel context.CancelFunc
}
func init() { func init() {
RegisterModule(ModuleRegistration{ RegisterModule(ModuleRegistration{
Type: "nats.server", Type: "nats.server",
@@ -24,22 +36,21 @@ func init() {
Type: "object", Type: "object",
Properties: map[string]*jsonschema.Schema{ Properties: map[string]*jsonschema.Schema{
"ip": { "ip": {
Title: "IP", Title: "IP",
Description: "the IP address to bind the NATS server to", Type: "string",
Type: "string", Default: json.RawMessage(`"0.0.0.0"`),
Default: json.RawMessage(`"0.0.0.0"`),
}, },
"port": { "port": {
Title: "Port", Title: "Port",
Description: "the port for the NATS server to listen on", Type: "integer",
Type: "integer", Minimum: jsonschema.Ptr[float64](1024),
Minimum: jsonschema.Ptr[float64](1024), Maximum: jsonschema.Ptr[float64](65535),
Maximum: jsonschema.Ptr[float64](65535), Default: json.RawMessage(`4222`),
Default: json.RawMessage(`4222`),
}, },
}, },
Required: []string{}, Required: []string{},
AdditionalProperties: &jsonschema.Schema{Not: &jsonschema.Schema{}},
AdditionalProperties: nil,
}, },
New: func(moduleConfig config.ModuleConfig) (common.Module, error) { New: func(moduleConfig config.ModuleConfig) (common.Module, error) {
params := moduleConfig.Params params := moduleConfig.Params
@@ -70,18 +81,6 @@ func init() {
}) })
} }
type NATSServer struct {
config config.ModuleConfig
ctx context.Context
Ip string
Port int
inputHandler common.InputHandler
server *server.Server
logger *slog.Logger
cancel context.CancelFunc
serverMu sync.Mutex
}
func (ns *NATSServer) Id() string { func (ns *NATSServer) Id() string {
return ns.config.Id return ns.config.Id
} }
@@ -90,46 +89,47 @@ func (ns *NATSServer) Type() string {
return ns.config.Type return ns.config.Type
} }
func (ns *NATSServer) Start(ctx context.Context, inputHandler common.InputHandler) error { func (ns *NATSServer) Start(ctx context.Context) error {
ns.logger.Debug("running") ns.logger.Debug("running")
ns.inputHandler = inputHandler router, ok := ctx.Value(common.RouterContextKey).(common.RouteIO)
if !ok {
return errors.New("nats.server unable to get router from context")
}
ns.router = router
moduleContext, cancel := context.WithCancel(ctx) moduleContext, cancel := context.WithCancel(ctx)
ns.ctx = moduleContext ns.ctx = moduleContext
ns.cancel = cancel ns.cancel = cancel
natsServer, err := server.NewServer(&server.Options{ natsServer, err := server.NewServer(&server.Options{
Host: ns.Ip, Host: ns.Ip,
Port: ns.Port, Port: ns.Port,
NoLog: true, NoLog: true,
NoSigs: true,
}) })
if err != nil { if err != nil {
return err return err
} }
ns.serverMu.Lock()
ns.server = natsServer ns.server = natsServer
natsServer.Start() natsServer.Start()
defer natsServer.Shutdown()
if !natsServer.ReadyForConnections(5 * time.Second) { if !natsServer.ReadyForConnections(5 * time.Second) {
return errors.New("nats.server failed to start") return errors.New("nats.server failed to start")
} }
ns.serverMu.Unlock() ns.logger.Info("NATS server started", "client_url", natsServer.ClientURL())
<-ns.ctx.Done() <-ns.ctx.Done()
ns.logger.Debug("done") ns.logger.Debug("done")
return nil return nil
} }
func (ns *NATSServer) Stop() { func (ns *NATSServer) Stop() {
if ns.cancel != nil { ns.cancel()
defer ns.cancel()
}
ns.serverMu.Lock()
defer ns.serverMu.Unlock()
if ns.server != nil { if ns.server != nil {
ns.server.Shutdown() ns.server.Shutdown()
ns.server.WaitForShutdown()
} }
} }
+30 -35
View File
@@ -1,10 +1,12 @@
//go:build !js
package module package module
import ( import (
"context" "context"
"errors"
"log/slog" "log/slog"
"net" "net"
"sync"
"time" "time"
"github.com/jwetzell/psn-go" "github.com/jwetzell/psn-go"
@@ -12,27 +14,27 @@ import (
"github.com/jwetzell/showbridge-go/internal/config" "github.com/jwetzell/showbridge-go/internal/config"
) )
type PSNClient struct {
config config.ModuleConfig
conn *net.UDPConn
ctx context.Context
router common.RouteIO
decoder *psn.Decoder
logger *slog.Logger
cancel context.CancelFunc
}
func init() { func init() {
RegisterModule(ModuleRegistration{ RegisterModule(ModuleRegistration{
Type: "psn.client", Type: "psn.client",
Title: "PosiStageNet Client", Title: "PosiStageNet Client",
New: func(config config.ModuleConfig) (common.Module, error) { New: func(config config.ModuleConfig) (common.Module, error) {
return &PSNClient{config: config, decoder: psn.NewDecoder(), logger: CreateLogger(config)}, nil return &PSNClient{config: config, decoder: psn.NewDecoder(), logger: CreateLogger(config)}, nil
}, },
}) })
} }
type PSNClient struct {
config config.ModuleConfig
conn *net.UDPConn
ctx context.Context
inputHandler common.InputHandler
decoder *psn.Decoder
logger *slog.Logger
cancel context.CancelFunc
connMu sync.Mutex
}
func (pc *PSNClient) Id() string { func (pc *PSNClient) Id() string {
return pc.config.Id return pc.config.Id
} }
@@ -41,9 +43,14 @@ func (pc *PSNClient) Type() string {
return pc.config.Type return pc.config.Type
} }
func (pc *PSNClient) Start(ctx context.Context, inputHandler common.InputHandler) error { func (pc *PSNClient) Start(ctx context.Context) error {
pc.logger.Debug("running") pc.logger.Debug("running")
pc.inputHandler = inputHandler router, ok := ctx.Value(common.RouterContextKey).(common.RouteIO)
if !ok {
return errors.New("psn.client unable to get router from context")
}
pc.router = router
moduleContext, cancel := context.WithCancel(ctx) moduleContext, cancel := context.WithCancel(ctx)
pc.ctx = moduleContext pc.ctx = moduleContext
pc.cancel = cancel pc.cancel = cancel
@@ -57,26 +64,24 @@ func (pc *PSNClient) Start(ctx context.Context, inputHandler common.InputHandler
if err != nil { if err != nil {
return err return err
} }
defer client.Close()
pc.connMu.Lock()
pc.conn = client pc.conn = client
pc.connMu.Unlock()
buffer := make([]byte, 2048) buffer := make([]byte, 2048)
for pc.ctx.Err() == nil { for {
select { select {
case <-pc.ctx.Done(): case <-pc.ctx.Done():
// TODO(jwetzell): cleanup?
pc.logger.Debug("done")
return nil return nil
default: default:
pc.connMu.Lock()
pc.conn.SetDeadline(time.Now().Add(time.Millisecond * 200)) pc.conn.SetDeadline(time.Now().Add(time.Millisecond * 200))
numBytes, _, err := pc.conn.ReadFromUDP(buffer) numBytes, _, err := pc.conn.ReadFromUDP(buffer)
pc.connMu.Unlock()
if err != nil { if err != nil {
//NOTE(jwetzell) we hit deadline //NOTE(jwetzell) we hit deadline
opErr, ok := err.(*net.OpError) if opErr, ok := err.(*net.OpError); ok && opErr.Timeout() {
if ok && opErr.Timeout() {
continue continue
} }
return err return err
@@ -89,29 +94,19 @@ func (pc *PSNClient) Start(ctx context.Context, inputHandler common.InputHandler
pc.logger.Error("problem decoding psn traffic", "error", err) pc.logger.Error("problem decoding psn traffic", "error", err)
} }
if pc.inputHandler != nil { if pc.router != nil {
// TODO(jwetzell): better input handling // TODO(jwetzell): better input handling
for _, tracker := range pc.decoder.Trackers { for _, tracker := range pc.decoder.Trackers {
pc.inputHandler(pc.ctx, pc.Id(), tracker) pc.router.HandleInput(pc.ctx, pc.Id(), tracker)
} }
} else { } else {
pc.logger.Error("has no input handler") pc.logger.Error("has no router")
} }
} }
} }
} }
<-pc.ctx.Done()
pc.logger.Debug("done")
return nil
} }
func (pc *PSNClient) Stop() { func (pc *PSNClient) Stop() {
if pc.cancel != nil { pc.cancel()
defer pc.cancel()
}
pc.connMu.Lock()
defer pc.connMu.Unlock()
if pc.conn != nil {
pc.conn.Close()
}
} }
+34 -39
View File
@@ -1,3 +1,5 @@
//go:build !js
package module package module
import ( import (
@@ -5,7 +7,6 @@ import (
"errors" "errors"
"fmt" "fmt"
"log/slog" "log/slog"
"sync"
"github.com/google/jsonschema-go/jsonschema" "github.com/google/jsonschema-go/jsonschema"
"github.com/jwetzell/showbridge-go/internal/common" "github.com/jwetzell/showbridge-go/internal/common"
@@ -13,6 +14,17 @@ import (
"github.com/redis/go-redis/v9" "github.com/redis/go-redis/v9"
) )
type RedisClient struct {
config config.ModuleConfig
ctx context.Context
router common.RouteIO
Host string
Port uint16
client *redis.Client
logger *slog.Logger
cancel context.CancelFunc
}
func init() { func init() {
RegisterModule(ModuleRegistration{ RegisterModule(ModuleRegistration{
Type: "redis.client", Type: "redis.client",
@@ -21,20 +33,16 @@ func init() {
Type: "object", Type: "object",
Properties: map[string]*jsonschema.Schema{ Properties: map[string]*jsonschema.Schema{
"host": { "host": {
Title: "Host", Type: "string",
Description: "the hostname or IP address of the Redis server to connect to",
Type: "string",
}, },
"port": { "port": {
Title: "Port", Type: "integer",
Description: "the port to use when connecting to the Redis server", Minimum: jsonschema.Ptr[float64](1),
Type: "integer", Maximum: jsonschema.Ptr[float64](65535),
Minimum: jsonschema.Ptr[float64](1),
Maximum: jsonschema.Ptr[float64](65535),
}, },
}, },
Required: []string{"host", "port"}, Required: []string{"host", "port"},
AdditionalProperties: &jsonschema.Schema{Not: &jsonschema.Schema{}}, AdditionalProperties: nil,
}, },
New: func(config config.ModuleConfig) (common.Module, error) { New: func(config config.ModuleConfig) (common.Module, error) {
params := config.Params params := config.Params
@@ -54,18 +62,6 @@ func init() {
}) })
} }
type RedisClient struct {
config config.ModuleConfig
ctx context.Context
inputHandler common.InputHandler
Host string
Port uint16
client *redis.Client
logger *slog.Logger
cancel context.CancelFunc
clientMu sync.Mutex
}
func (rc *RedisClient) Id() string { func (rc *RedisClient) Id() string {
return rc.config.Id return rc.config.Id
} }
@@ -74,15 +70,21 @@ func (rc *RedisClient) Type() string {
return rc.config.Type return rc.config.Type
} }
func (rc *RedisClient) Printf(ctx context.Context, format string, v ...any) { func (rc *RedisClient) Printf(ctx context.Context, format string, v ...interface{}) {
msg := fmt.Sprintf(format, v...) msg := fmt.Sprintf(format, v...)
rc.logger.Debug(msg) rc.logger.Debug(msg)
} }
func (rc *RedisClient) Start(ctx context.Context, inputHandler common.InputHandler) error { func (rc *RedisClient) Start(ctx context.Context) error {
redis.SetLogger(rc) redis.SetLogger(rc)
rc.logger.Debug("running") rc.logger.Debug("running")
rc.inputHandler = inputHandler router, ok := ctx.Value(common.RouterContextKey).(common.RouteIO)
if !ok {
return errors.New("redis.client unable to get router from context")
}
rc.router = router
moduleContext, cancel := context.WithCancel(ctx) moduleContext, cancel := context.WithCancel(ctx)
rc.ctx = moduleContext rc.ctx = moduleContext
rc.cancel = cancel rc.cancel = cancel
@@ -93,9 +95,9 @@ func (rc *RedisClient) Start(ctx context.Context, inputHandler common.InputHandl
DB: 0, DB: 0,
}) })
rc.clientMu.Lock()
rc.client = client rc.client = client
rc.clientMu.Unlock()
defer client.Close()
<-rc.ctx.Done() <-rc.ctx.Done()
rc.logger.Debug("done") rc.logger.Debug("done")
@@ -103,19 +105,12 @@ func (rc *RedisClient) Start(ctx context.Context, inputHandler common.InputHandl
} }
func (rc *RedisClient) Stop() { func (rc *RedisClient) Stop() {
if rc.cancel != nil { rc.cancel()
defer rc.cancel()
}
rc.clientMu.Lock()
defer rc.clientMu.Unlock()
if rc.client != nil {
rc.client.Close()
}
} }
func (rc *RedisClient) Get(ctx context.Context, key string) (any, error) { func (rc *RedisClient) Get(key string) (any, error) {
if rc.client != nil { if rc.client != nil {
val, err := rc.client.Get(ctx, key).Result() val, err := rc.client.Get(rc.ctx, key).Result()
if err != nil { if err != nil {
return nil, err return nil, err
} }
@@ -124,9 +119,9 @@ func (rc *RedisClient) Get(ctx context.Context, key string) (any, error) {
return nil, errors.New("redis.client not setup") return nil, errors.New("redis.client not setup")
} }
func (rc *RedisClient) Set(ctx context.Context, key string, value any) error { func (rc *RedisClient) Set(key string, value any) error {
if rc.client != nil { if rc.client != nil {
status := rc.client.Set(ctx, key, value, 0) status := rc.client.Set(rc.ctx, key, value, 0)
return status.Err() return status.Err()
} }
return errors.New("redis.client not setup") return errors.New("redis.client not setup")
+53 -67
View File
@@ -7,7 +7,6 @@ import (
"errors" "errors"
"fmt" "fmt"
"log/slog" "log/slog"
"sync"
"time" "time"
"github.com/google/jsonschema-go/jsonschema" "github.com/google/jsonschema-go/jsonschema"
@@ -17,6 +16,18 @@ import (
"go.bug.st/serial" "go.bug.st/serial"
) )
type SerialClient struct {
config config.ModuleConfig
ctx context.Context
router common.RouteIO
Port string
Framer framer.Framer
Mode *serial.Mode
port serial.Port
logger *slog.Logger
cancel context.CancelFunc
}
func init() { func init() {
RegisterModule(ModuleRegistration{ RegisterModule(ModuleRegistration{
Type: "serial.client", Type: "serial.client",
@@ -25,24 +36,16 @@ func init() {
Type: "object", Type: "object",
Properties: map[string]*jsonschema.Schema{ Properties: map[string]*jsonschema.Schema{
"port": { "port": {
Title: "Port", Title: "Port",
Description: "the name of the serial port to connect to", Type: "string",
Type: "string",
}, },
"baudRate": { "baudRate": {
Title: "Baud Rate", Title: "Baud Rate",
Description: "the baud rate to use when connecting to the serial port", Type: "integer",
Type: "integer",
},
"framing": {
Title: "Framing Method",
Description: "the method to use for framing messages on the serial port",
Type: "string",
Enum: []any{"LF", "CR", "CRLF", "SLIP", "RAW"},
}, },
}, },
Required: []string{"port", "baudRate", "framing"}, Required: []string{"port", "baudRate"},
AdditionalProperties: &jsonschema.Schema{Not: &jsonschema.Schema{}}, AdditionalProperties: nil,
}, },
New: func(config config.ModuleConfig) (common.Module, error) { New: func(config config.ModuleConfig) (common.Module, error) {
params := config.Params params := config.Params
@@ -56,14 +59,12 @@ func init() {
return nil, fmt.Errorf("serial.client framing error: %w", err) return nil, fmt.Errorf("serial.client framing error: %w", err)
} }
inFramer := framer.GetFramer(framingMethodString) framer := framer.GetFramer(framingMethodString)
if inFramer == nil { if framer == nil {
return nil, fmt.Errorf("serial.client unknown framing method: %s", framingMethodString) return nil, fmt.Errorf("serial.client unknown framing method: %s", framingMethodString)
} }
outFramer := framer.GetFramer(framingMethodString)
baudRateInt, err := params.GetInt("baudRate") baudRateInt, err := params.GetInt("baudRate")
if err != nil { if err != nil {
return nil, fmt.Errorf("serial.client baudRate error: %w", err) return nil, fmt.Errorf("serial.client baudRate error: %w", err)
@@ -73,25 +74,11 @@ func init() {
BaudRate: baudRateInt, BaudRate: baudRateInt,
} }
return &SerialClient{config: config, Port: portString, inFramer: inFramer, outFramer: outFramer, Mode: &mode, logger: CreateLogger(config)}, nil return &SerialClient{config: config, Port: portString, Framer: framer, Mode: &mode, logger: CreateLogger(config)}, nil
}, },
}) })
} }
type SerialClient struct {
config config.ModuleConfig
ctx context.Context
inputHandler common.InputHandler
Port string
inFramer framer.Framer
outFramer framer.Framer
Mode *serial.Mode
port serial.Port
logger *slog.Logger
cancel context.CancelFunc
portMu sync.Mutex
}
func (sc *SerialClient) Id() string { func (sc *SerialClient) Id() string {
return sc.config.Id return sc.config.Id
} }
@@ -101,11 +88,10 @@ func (sc *SerialClient) Type() string {
} }
func (sc *SerialClient) SetupPort() error { func (sc *SerialClient) SetupPort() error {
sc.portMu.Lock()
defer sc.portMu.Unlock()
port, err := serial.Open(sc.Port, sc.Mode) port, err := serial.Open(sc.Port, sc.Mode)
if err != nil { if err != nil {
return err return fmt.Errorf("serial.client can't open input port: %s", sc.Port)
} }
sc.port = port sc.port = port
@@ -113,20 +99,36 @@ func (sc *SerialClient) SetupPort() error {
return nil return nil
} }
func (sc *SerialClient) Start(ctx context.Context, inputHandler common.InputHandler) error { func (sc *SerialClient) Start(ctx context.Context) error {
sc.logger.Debug("running") sc.logger.Debug("running")
sc.inputHandler = inputHandler router, ok := ctx.Value(common.RouterContextKey).(common.RouteIO)
if !ok {
return errors.New("serial.client unable to get router from context")
}
sc.router = router
moduleContext, cancel := context.WithCancel(ctx) moduleContext, cancel := context.WithCancel(ctx)
sc.ctx = moduleContext sc.ctx = moduleContext
sc.cancel = cancel sc.cancel = cancel
for sc.ctx.Err() == nil { // TODO(jwetzell): shutdown with router.Context properly
go func() {
<-sc.ctx.Done()
sc.logger.Debug("done")
if sc.port != nil {
sc.port.Close()
}
}()
for {
err := sc.SetupPort() err := sc.SetupPort()
if err != nil { if err != nil {
if sc.ctx.Err() != nil { if sc.ctx.Err() != nil {
sc.logger.Debug("done")
return nil return nil
} }
sc.logger.Error("port setup error", "port", sc.Port, "error", err.Error()) sc.logger.Error("port setup error", "error", err.Error())
time.Sleep(time.Second * 2) time.Sleep(time.Second * 2)
continue continue
} }
@@ -134,27 +136,29 @@ func (sc *SerialClient) Start(ctx context.Context, inputHandler common.InputHand
buffer := make([]byte, 1024) buffer := make([]byte, 1024)
select { select {
case <-sc.ctx.Done(): case <-sc.ctx.Done():
sc.logger.Debug("done")
return nil return nil
default: default:
READ: READ:
for sc.ctx.Err() == nil { for {
select { select {
case <-sc.ctx.Done(): case <-sc.ctx.Done():
sc.logger.Debug("done")
return nil return nil
default: default:
byteCount, err := sc.port.Read(buffer) byteCount, err := sc.port.Read(buffer)
if err != nil { if err != nil {
sc.inFramer.Clear() sc.Framer.Clear()
break READ break READ
} }
if sc.inFramer != nil { if sc.Framer != nil {
if byteCount > 0 { if byteCount > 0 {
messages := sc.inFramer.Decode(buffer[0:byteCount]) messages := sc.Framer.Decode(buffer[0:byteCount])
for _, message := range messages { for _, message := range messages {
if sc.inputHandler != nil { if sc.router != nil {
sc.inputHandler(sc.ctx, sc.Id(), message) sc.router.HandleInput(sc.ctx, sc.Id(), message)
} else { } else {
sc.logger.Error("input received but no router is configured") sc.logger.Error("input received but no router is configured")
} }
@@ -165,9 +169,6 @@ func (sc *SerialClient) Start(ctx context.Context, inputHandler common.InputHand
} }
} }
} }
<-sc.ctx.Done()
sc.logger.Debug("done")
return nil
} }
func (sc *SerialClient) Output(ctx context.Context, payload any) error { func (sc *SerialClient) Output(ctx context.Context, payload any) error {
@@ -178,25 +179,10 @@ func (sc *SerialClient) Output(ctx context.Context, payload any) error {
return errors.New("serial.client can only output bytes") return errors.New("serial.client can only output bytes")
} }
_, err := sc.port.Write(sc.outFramer.Encode(payloadBytes)) _, err := sc.port.Write(sc.Framer.Encode(payloadBytes))
return err return err
} }
func (sc *SerialClient) Stop() { func (sc *SerialClient) Stop() {
if sc.cancel != nil { sc.cancel()
defer sc.cancel()
}
sc.portMu.Lock()
defer sc.portMu.Unlock()
if sc.port != nil {
sc.port.Close()
}
if sc.inFramer != nil {
sc.inFramer.Clear()
}
if sc.outFramer != nil {
sc.outFramer.Clear()
}
} }
+62 -73
View File
@@ -1,3 +1,5 @@
//go:build !js
package module package module
import ( import (
@@ -21,6 +23,30 @@ import (
"github.com/jwetzell/showbridge-go/internal/processor" "github.com/jwetzell/showbridge-go/internal/processor"
) )
type SIPCallServer struct {
config config.ModuleConfig
ctx context.Context
router common.RouteIO
IP string
Port int
Transport string
UserAgent string
dg *diago.Diago
logger *slog.Logger
cancel context.CancelFunc
}
type SIPCallMessage struct {
To string
}
type SIPCall struct {
inDialog *diago.DialogServerSession
lock sync.Mutex
}
type sipCallContextKey string
func init() { func init() {
RegisterModule(ModuleRegistration{ RegisterModule(ModuleRegistration{
Type: "sip.call.server", Type: "sip.call.server",
@@ -29,35 +55,31 @@ func init() {
Type: "object", Type: "object",
Properties: map[string]*jsonschema.Schema{ Properties: map[string]*jsonschema.Schema{
"ip": { "ip": {
Title: "IP", Title: "IP",
Description: "the IP address to bind the SIP server to", Type: "string",
Type: "string", Default: json.RawMessage(`"0.0.0.0"`),
Default: json.RawMessage(`"0.0.0.0"`),
}, },
"port": { "port": {
Title: "Port", Title: "Port",
Description: "the port for the SIP server to listen on", Type: "integer",
Type: "integer", Minimum: jsonschema.Ptr[float64](1024),
Minimum: jsonschema.Ptr[float64](1024), Maximum: jsonschema.Ptr[float64](65535),
Maximum: jsonschema.Ptr[float64](65535), Default: json.RawMessage(`5060`),
Default: json.RawMessage(`5060`),
}, },
"transport": { "transport": {
Title: "Transport", Title: "Transport",
Description: "the transport protocol to use for the SIP server", Type: "string",
Type: "string", Enum: []any{"udp", "tcp", "ws", "udp4", "tcp4"},
Enum: []any{"udp", "tcp", "ws", "udp4", "tcp4"}, Default: json.RawMessage(`"udp"`),
Default: json.RawMessage(`"udp"`),
}, },
"userAgent": { "userAgent": {
Title: "User Agent", Title: "User Agent",
Description: "the user agent string to use", Type: "string",
Type: "string", Default: json.RawMessage(`"showbridge"`),
Default: json.RawMessage(`"showbridge"`),
}, },
}, },
Required: []string{}, Required: []string{},
AdditionalProperties: &jsonschema.Schema{Not: &jsonschema.Schema{}}, AdditionalProperties: nil,
}, },
New: func(moduleConfig config.ModuleConfig) (common.Module, error) { New: func(moduleConfig config.ModuleConfig) (common.Module, error) {
params := moduleConfig.Params params := moduleConfig.Params
@@ -103,31 +125,6 @@ func init() {
}) })
} }
type SIPCallServer struct {
config config.ModuleConfig
ctx context.Context
inputHandler common.InputHandler
IP string
Port int
Transport string
UserAgent string
logger *slog.Logger
cancel context.CancelFunc
ua *sipgo.UserAgent
uaMu sync.Mutex
}
type SIPCallMessage struct {
To string
}
type SIPCall struct {
inDialog *diago.DialogServerSession
lock sync.Mutex
}
type sipCallContextKey string
func (scs *SIPCallServer) Id() string { func (scs *SIPCallServer) Id() string {
return scs.config.Id return scs.config.Id
} }
@@ -136,9 +133,14 @@ func (scs *SIPCallServer) Type() string {
return scs.config.Type return scs.config.Type
} }
func (scs *SIPCallServer) Start(ctx context.Context, inputHandler common.InputHandler) error { func (scs *SIPCallServer) Start(ctx context.Context) error {
scs.logger.Debug("running") scs.logger.Debug("running")
scs.inputHandler = inputHandler router, ok := ctx.Value(common.RouterContextKey).(common.RouteIO)
if !ok {
return errors.New("sip.call.server unable to get router from context")
}
scs.router = router
moduleContext, cancel := context.WithCancel(ctx) moduleContext, cancel := context.WithCancel(ctx)
scs.ctx = moduleContext scs.ctx = moduleContext
scs.cancel = cancel scs.cancel = cancel
@@ -150,9 +152,7 @@ func (scs *SIPCallServer) Start(ctx context.Context, inputHandler common.InputHa
sipgo.WithUserAgentTransportLayerOptions(sip.WithTransportLayerLogger(diagoLogger)), sipgo.WithUserAgentTransportLayerOptions(sip.WithTransportLayerLogger(diagoLogger)),
sipgo.WithUserAgentTransactionLayerOptions(sip.WithTransactionLayerLogger(diagoLogger)), sipgo.WithUserAgentTransactionLayerOptions(sip.WithTransactionLayerLogger(diagoLogger)),
) )
scs.uaMu.Lock() defer ua.Close()
scs.ua = ua
scs.uaMu.Unlock()
sip.SetDefaultLogger(diagoLogger) sip.SetDefaultLogger(diagoLogger)
media.SetDefaultLogger(diagoLogger) media.SetDefaultLogger(diagoLogger)
@@ -164,12 +164,13 @@ func (scs *SIPCallServer) Start(ctx context.Context, inputHandler common.InputHa
}, },
)) ))
err := dg.Serve(scs.ctx, func(inDialog *diago.DialogServerSession) { go func() {
scs.HandleCall(inDialog) dg.Serve(scs.ctx, func(inDialog *diago.DialogServerSession) {
}) scs.HandleCall(inDialog)
if err != nil { })
scs.logger.Error("diago serve error", "error", err) }()
}
scs.dg = dg
<-scs.ctx.Done() <-scs.ctx.Done()
scs.logger.Debug("done") scs.logger.Debug("done")
@@ -184,11 +185,9 @@ func (scs *SIPCallServer) HandleCall(inDialog *diago.DialogServerSession) {
dialogContext := context.WithValue(scs.ctx, sipCallContextKey("call"), &SIPCall{ dialogContext := context.WithValue(scs.ctx, sipCallContextKey("call"), &SIPCall{
inDialog: inDialog, inDialog: inDialog,
}) })
if scs.inputHandler != nil { scs.router.HandleInput(dialogContext, scs.Id(), SIPCallMessage{
scs.inputHandler(dialogContext, scs.Id(), SIPCallMessage{ To: inDialog.ToUser(),
To: inDialog.ToUser(), })
})
}
} }
func (scs *SIPCallServer) Output(ctx context.Context, payload any) error { func (scs *SIPCallServer) Output(ctx context.Context, payload any) error {
@@ -212,10 +211,7 @@ func (scs *SIPCallServer) Output(ctx context.Context, payload any) error {
payloadDTMFResponse, ok := common.GetAnyAs[processor.SipDTMFResponse](payload) payloadDTMFResponse, ok := common.GetAnyAs[processor.SipDTMFResponse](payload)
if ok { if ok {
dtmfWriter, err := call.inDialog.AudioWriterDTMF() dtmfWriter := call.inDialog.AudioWriterDTMF()
if err != nil {
return err
}
time.Sleep(time.Millisecond * time.Duration(payloadDTMFResponse.PreWait)) time.Sleep(time.Millisecond * time.Duration(payloadDTMFResponse.PreWait))
for i, dtmfRune := range payloadDTMFResponse.Digits { for i, dtmfRune := range payloadDTMFResponse.Digits {
err := dtmfWriter.WriteDTMF(dtmfRune) err := dtmfWriter.WriteDTMF(dtmfRune)
@@ -258,12 +254,5 @@ func (scs *SIPCallServer) Output(ctx context.Context, payload any) error {
} }
func (scs *SIPCallServer) Stop() { func (scs *SIPCallServer) Stop() {
if scs.cancel != nil { scs.cancel()
defer scs.cancel()
}
scs.uaMu.Lock()
defer scs.uaMu.Unlock()
if scs.ua != nil {
scs.ua.Close()
}
} }
+58 -74
View File
@@ -1,3 +1,5 @@
//go:build !js
package module package module
import ( import (
@@ -22,6 +24,29 @@ import (
"github.com/jwetzell/showbridge-go/internal/processor" "github.com/jwetzell/showbridge-go/internal/processor"
) )
type SIPDTMFServer struct {
config config.ModuleConfig
ctx context.Context
router common.RouteIO
IP string
Port int
Transport string
UserAgent string
Separator string
logger *slog.Logger
cancel context.CancelFunc
}
type SIPDTMFMessage struct {
To string
Digits string
}
type SIPDTMFCall struct {
inDialog *diago.DialogServerSession
lock sync.Mutex
}
func init() { func init() {
RegisterModule(ModuleRegistration{ RegisterModule(ModuleRegistration{
Type: "sip.dtmf.server", Type: "sip.dtmf.server",
@@ -30,42 +55,37 @@ func init() {
Type: "object", Type: "object",
Properties: map[string]*jsonschema.Schema{ Properties: map[string]*jsonschema.Schema{
"ip": { "ip": {
Title: "IP", Title: "IP",
Description: "the IP address to bind the SIP server to", Type: "string",
Type: "string", Default: json.RawMessage(`"0.0.0.0"`),
Default: json.RawMessage(`"0.0.0.0"`),
}, },
"port": { "port": {
Title: "Port", Title: "Port",
Description: "the port for the SIP server to listen on", Type: "integer",
Type: "integer", Minimum: jsonschema.Ptr[float64](1024),
Minimum: jsonschema.Ptr[float64](1024), Maximum: jsonschema.Ptr[float64](65535),
Maximum: jsonschema.Ptr[float64](65535), Default: json.RawMessage(`5060`),
Default: json.RawMessage(`5060`),
}, },
"transport": { "transport": {
Title: "Transport", Title: "Transport",
Description: "the transport protocol to use for the SIP server", Type: "string",
Type: "string", Enum: []any{"udp", "tcp", "ws", "udp4", "tcp4"},
Enum: []any{"udp", "tcp", "ws", "udp4", "tcp4"}, Default: json.RawMessage(`"udp"`),
Default: json.RawMessage(`"udp"`),
}, },
"userAgent": { "userAgent": {
Title: "User Agent", Title: "User Agent",
Description: "the user agent string to use", Type: "string",
Type: "string", Default: json.RawMessage(`"showbridge"`),
Default: json.RawMessage(`"showbridge"`),
}, },
"separator": { "separator": {
Title: "DTMF Separator", Title: "DTMF Separator",
Description: "the DTMF character to use as a separator between DTMF digit groups", Type: "string",
Type: "string", MinLength: jsonschema.Ptr(1),
MinLength: new(1), MaxLength: jsonschema.Ptr(1),
MaxLength: new(1),
}, },
}, },
Required: []string{"separator"}, Required: []string{"separator"},
AdditionalProperties: &jsonschema.Schema{Not: &jsonschema.Schema{}}, AdditionalProperties: nil,
}, },
New: func(moduleConfig config.ModuleConfig) (common.Module, error) { New: func(moduleConfig config.ModuleConfig) (common.Module, error) {
params := moduleConfig.Params params := moduleConfig.Params
@@ -124,31 +144,6 @@ func init() {
}) })
} }
type SIPDTMFServer struct {
config config.ModuleConfig
ctx context.Context
inputHandler common.InputHandler
IP string
Port int
Transport string
UserAgent string
Separator string
logger *slog.Logger
cancel context.CancelFunc
ua *sipgo.UserAgent
uaMu sync.Mutex
}
type SIPDTMFMessage struct {
To string
Digits string
}
type SIPDTMFCall struct {
inDialog *diago.DialogServerSession
lock sync.Mutex
}
func (sds *SIPDTMFServer) Id() string { func (sds *SIPDTMFServer) Id() string {
return sds.config.Id return sds.config.Id
} }
@@ -157,9 +152,14 @@ func (sds *SIPDTMFServer) Type() string {
return sds.config.Type return sds.config.Type
} }
func (sds *SIPDTMFServer) Start(ctx context.Context, inputHandler common.InputHandler) error { func (sds *SIPDTMFServer) Start(ctx context.Context) error {
sds.logger.Debug("running") sds.logger.Debug("running")
sds.inputHandler = inputHandler router, ok := ctx.Value(common.RouterContextKey).(common.RouteIO)
if !ok {
return errors.New("sip.dtmf.server unable to get router from context")
}
sds.router = router
moduleContext, cancel := context.WithCancel(ctx) moduleContext, cancel := context.WithCancel(ctx)
sds.ctx = moduleContext sds.ctx = moduleContext
sds.cancel = cancel sds.cancel = cancel
@@ -171,10 +171,7 @@ func (sds *SIPDTMFServer) Start(ctx context.Context, inputHandler common.InputHa
sipgo.WithUserAgentTransportLayerOptions(sip.WithTransportLayerLogger(diagoLogger)), sipgo.WithUserAgentTransportLayerOptions(sip.WithTransportLayerLogger(diagoLogger)),
sipgo.WithUserAgentTransactionLayerOptions(sip.WithTransactionLayerLogger(diagoLogger)), sipgo.WithUserAgentTransactionLayerOptions(sip.WithTransactionLayerLogger(diagoLogger)),
) )
defer ua.Close()
sds.uaMu.Lock()
sds.ua = ua
sds.uaMu.Unlock()
sip.SetDefaultLogger(diagoLogger) sip.SetDefaultLogger(diagoLogger)
media.SetDefaultLogger(diagoLogger) media.SetDefaultLogger(diagoLogger)
@@ -204,19 +201,16 @@ func (sds *SIPDTMFServer) HandleCall(inDialog *diago.DialogServerSession) error
inDialog.Ringing() inDialog.Ringing()
inDialog.Answer() inDialog.Answer()
reader, err := inDialog.AudioReaderDTMF() reader := inDialog.AudioReaderDTMF()
if err != nil {
return err
}
userString := "" userString := ""
return reader.Listen(func(dtmf rune) error { return reader.Listen(func(dtmf rune) error {
if dtmf == rune(sds.Separator[0]) { if dtmf == rune(sds.Separator[0]) {
if sds.inputHandler != nil { if sds.router != nil {
dialogContext := context.WithValue(sds.ctx, sipCallContextKey("call"), &SIPDTMFCall{ dialogContext := context.WithValue(sds.ctx, sipCallContextKey("call"), &SIPDTMFCall{
inDialog: inDialog, inDialog: inDialog,
}) })
sds.inputHandler(dialogContext, sds.Id(), SIPDTMFMessage{ sds.router.HandleInput(dialogContext, sds.Id(), SIPDTMFMessage{
To: inDialog.ToUser(), To: inDialog.ToUser(),
Digits: userString, Digits: userString,
}) })
@@ -249,10 +243,7 @@ func (sds *SIPDTMFServer) Output(ctx context.Context, payload any) error {
payloadDTMFResponse, ok := common.GetAnyAs[processor.SipDTMFResponse](payload) payloadDTMFResponse, ok := common.GetAnyAs[processor.SipDTMFResponse](payload)
if ok { if ok {
dtmfWriter, err := call.inDialog.AudioWriterDTMF() dtmfWriter := call.inDialog.AudioWriterDTMF()
if err != nil {
return err
}
time.Sleep(time.Millisecond * time.Duration(payloadDTMFResponse.PreWait)) time.Sleep(time.Millisecond * time.Duration(payloadDTMFResponse.PreWait))
for i, dtmfRune := range payloadDTMFResponse.Digits { for i, dtmfRune := range payloadDTMFResponse.Digits {
@@ -297,12 +288,5 @@ func (sds *SIPDTMFServer) Output(ctx context.Context, payload any) error {
} }
func (sds *SIPDTMFServer) Stop() { func (sds *SIPDTMFServer) Stop() {
if sds.cancel != nil { sds.cancel()
defer sds.cancel()
}
sds.uaMu.Lock()
defer sds.uaMu.Unlock()
if sds.ua != nil {
sds.ua.Close()
}
} }
+78 -87
View File
@@ -1,3 +1,5 @@
//go:build !js
package module package module
import ( import (
@@ -6,7 +8,6 @@ import (
"fmt" "fmt"
"log/slog" "log/slog"
"net" "net"
"sync"
"time" "time"
"github.com/google/jsonschema-go/jsonschema" "github.com/google/jsonschema-go/jsonschema"
@@ -15,6 +16,17 @@ import (
"github.com/jwetzell/showbridge-go/internal/framer" "github.com/jwetzell/showbridge-go/internal/framer"
) )
type TCPClient struct {
config config.ModuleConfig
framer framer.Framer
conn *net.TCPConn
ctx context.Context
router common.RouteIO
Addr *net.TCPAddr
logger *slog.Logger
cancel context.CancelFunc
}
func init() { func init() {
RegisterModule(ModuleRegistration{ RegisterModule(ModuleRegistration{
Type: "net.tcp.client", Type: "net.tcp.client",
@@ -23,26 +35,23 @@ func init() {
Type: "object", Type: "object",
Properties: map[string]*jsonschema.Schema{ Properties: map[string]*jsonschema.Schema{
"host": { "host": {
Title: "Host", Title: "Host",
Description: "the hostname or IP address of the TCP server to connect to", Type: "string",
Type: "string",
}, },
"port": { "port": {
Title: "Port", Title: "Port",
Description: "the port of the TCP server to connect to", Type: "integer",
Type: "integer", Minimum: jsonschema.Ptr[float64](1),
Minimum: jsonschema.Ptr[float64](1), Maximum: jsonschema.Ptr[float64](65535),
Maximum: jsonschema.Ptr[float64](65535),
}, },
"framing": { "framing": {
Title: "Framing Method", Title: "Framing Method",
Description: "the method used to frame messages over the TCP connection", Type: "string",
Type: "string", Enum: []any{"LF", "CR", "CRLF", "SLIP", "RAW"},
Enum: []any{"LF", "CR", "CRLF", "SLIP", "RAW"},
}, },
}, },
Required: []string{"host", "port", "framing"}, Required: []string{"host", "port", "framing"},
AdditionalProperties: &jsonschema.Schema{Not: &jsonschema.Schema{}}, AdditionalProperties: nil,
}, },
New: func(config config.ModuleConfig) (common.Module, error) { New: func(config config.ModuleConfig) (common.Module, error) {
params := config.Params params := config.Params
@@ -66,31 +75,16 @@ func init() {
return nil, fmt.Errorf("net.tcp.client framing error: %w", err) return nil, fmt.Errorf("net.tcp.client framing error: %w", err)
} }
inFramer := framer.GetFramer(framingMethodString) framer := framer.GetFramer(framingMethodString)
if inFramer == nil { if framer == nil {
return nil, fmt.Errorf("net.tcp.client unknown framing method: %s", framingMethodString) return nil, fmt.Errorf("net.tcp.client unknown framing method: %s", framingMethodString)
} }
return &TCPClient{framer: framer, Addr: addr, config: config, logger: CreateLogger(config)}, nil
outFramer := framer.GetFramer(framingMethodString)
return &TCPClient{inFramer: inFramer, outFramer: outFramer, Addr: addr, config: config, logger: CreateLogger(config)}, nil
}, },
}) })
} }
type TCPClient struct {
config config.ModuleConfig
inFramer framer.Framer
outFramer framer.Framer
conn *net.TCPConn
ctx context.Context
inputHandler common.InputHandler
Addr *net.TCPAddr
logger *slog.Logger
cancel context.CancelFunc
connMu sync.Mutex
}
func (tc *TCPClient) Id() string { func (tc *TCPClient) Id() string {
return tc.config.Id return tc.config.Id
} }
@@ -99,19 +93,33 @@ func (tc *TCPClient) Type() string {
return tc.config.Type return tc.config.Type
} }
func (tc *TCPClient) Start(ctx context.Context, inputHandler common.InputHandler) error { func (tc *TCPClient) Start(ctx context.Context) error {
tc.logger.Debug("running") tc.logger.Debug("running")
tc.inputHandler = inputHandler router, ok := ctx.Value(common.RouterContextKey).(common.RouteIO)
if !ok {
return errors.New("net.tcp.client unable to get router from context")
}
tc.router = router
moduleContext, cancel := context.WithCancel(ctx) moduleContext, cancel := context.WithCancel(ctx)
tc.ctx = moduleContext tc.ctx = moduleContext
tc.cancel = cancel tc.cancel = cancel
CONNECT_RETRY: // TODO(jwetzell): shutdown with router.Context properly
for tc.ctx.Err() == nil { go func() {
<-tc.ctx.Done()
tc.logger.Debug("done")
if tc.conn != nil {
tc.conn.Close()
}
}()
for {
err := tc.SetupConn() err := tc.SetupConn()
if err != nil { if err != nil {
if tc.ctx.Err() != nil { if tc.ctx.Err() != nil {
break CONNECT_RETRY tc.logger.Debug("done")
return nil
} }
tc.logger.Error("connection error", "error", err.Error()) tc.logger.Error("connection error", "error", err.Error())
time.Sleep(time.Second * 2) time.Sleep(time.Second * 2)
@@ -119,82 +127,65 @@ CONNECT_RETRY:
} }
buffer := make([]byte, 1024) buffer := make([]byte, 1024)
select {
case <-tc.ctx.Done():
tc.logger.Debug("done")
return nil
default:
READ:
for {
select {
case <-tc.ctx.Done():
tc.logger.Debug("done")
return nil
default:
byteCount, err := tc.conn.Read(buffer)
READ: if err != nil {
for tc.ctx.Err() == nil { tc.framer.Clear()
tc.conn.SetReadDeadline(time.Now().Add(time.Millisecond * 200)) break READ
byteCount, err := tc.conn.Read(buffer)
if err != nil {
opErr, ok := err.(*net.OpError)
if ok {
//NOTE(jwetzell) we hit deadline
if opErr.Timeout() {
continue
} }
}
if errors.Is(err, net.ErrClosed) {
break CONNECT_RETRY
}
break READ
}
if tc.inFramer != nil { if tc.framer != nil {
if byteCount > 0 { if byteCount > 0 {
messages := tc.inFramer.Decode(buffer[0:byteCount]) messages := tc.framer.Decode(buffer[0:byteCount])
for _, message := range messages { for _, message := range messages {
if tc.inputHandler != nil { if tc.router != nil {
tc.inputHandler(tc.ctx, tc.Id(), message) tc.router.HandleInput(tc.ctx, tc.Id(), message)
} else { } else {
tc.logger.Error("input received but no input handler is configured") tc.logger.Error("input received but no router is configured")
}
}
} }
} }
} }
} }
} }
} }
<-tc.ctx.Done()
tc.logger.Debug("done")
return nil
} }
func (tc *TCPClient) SetupConn() error { func (tc *TCPClient) SetupConn() error {
tc.connMu.Lock()
defer tc.connMu.Unlock()
client, err := net.DialTCP("tcp", nil, tc.Addr) client, err := net.DialTCP("tcp", nil, tc.Addr)
tc.conn = client tc.conn = client
return err return err
} }
func (tc *TCPClient) Output(ctx context.Context, payload any) error { func (tc *TCPClient) Output(ctx context.Context, payload any) error {
tc.connMu.Lock() // NOTE(jwetzell): not sure how this would occur but
defer tc.connMu.Unlock()
if tc.conn == nil { if tc.conn == nil {
return errors.New("net.tcp.client client is not setup") err := tc.SetupConn()
if err != nil {
return err
}
} }
payloadBytes, ok := common.GetAnyAsByteSlice(payload) payloadBytes, ok := common.GetAnyAsByteSlice(payload)
if !ok { if !ok {
return errors.New("net.tcp.client is only able to output bytes") return errors.New("net.tcp.client is only able to output bytes")
} }
_, err := tc.conn.Write(tc.inFramer.Encode(payloadBytes)) _, err := tc.conn.Write(tc.framer.Encode(payloadBytes))
return err return err
} }
func (tc *TCPClient) Stop() { func (tc *TCPClient) Stop() {
if tc.cancel != nil { tc.cancel()
defer tc.cancel()
}
tc.connMu.Lock()
defer tc.connMu.Unlock()
if tc.conn != nil {
tc.conn.Close()
}
if tc.inFramer != nil {
tc.inFramer.Clear()
}
if tc.outFramer != nil {
tc.outFramer.Clear()
}
} }
+125 -121
View File
@@ -1,3 +1,5 @@
//go:build !js
package module package module
import ( import (
@@ -8,8 +10,8 @@ import (
"log/slog" "log/slog"
"net" "net"
"slices" "slices"
"strings"
"sync" "sync"
"syscall"
"time" "time"
"github.com/google/jsonschema-go/jsonschema" "github.com/google/jsonschema-go/jsonschema"
@@ -18,6 +20,20 @@ import (
"github.com/jwetzell/showbridge-go/internal/framer" "github.com/jwetzell/showbridge-go/internal/framer"
) )
type TCPServer struct {
config config.ModuleConfig
Addr *net.TCPAddr
Framer framer.Framer
ctx context.Context
router common.RouteIO
quit chan interface{}
wg sync.WaitGroup
connections []*net.TCPConn
connectionsMu sync.RWMutex
logger *slog.Logger
cancel context.CancelFunc
}
func init() { func init() {
RegisterModule(ModuleRegistration{ RegisterModule(ModuleRegistration{
Type: "net.tcp.server", Type: "net.tcp.server",
@@ -26,27 +42,24 @@ func init() {
Type: "object", Type: "object",
Properties: map[string]*jsonschema.Schema{ Properties: map[string]*jsonschema.Schema{
"ip": { "ip": {
Title: "IP", Title: "IP",
Description: "the IP address to bind the TCP server to", Type: "string",
Type: "string", Default: json.RawMessage(`"0.0.0.0"`),
Default: json.RawMessage(`"0.0.0.0"`),
}, },
"port": { "port": {
Title: "Port", Title: "Port",
Description: "the port for the TCP server to listen on", Type: "integer",
Type: "integer", Minimum: jsonschema.Ptr[float64](1024),
Minimum: jsonschema.Ptr[float64](1024), Maximum: jsonschema.Ptr[float64](65535),
Maximum: jsonschema.Ptr[float64](65535),
}, },
"framing": { "framing": {
Title: "Framing Method", Title: "Framing Method",
Description: "the method used to frame messages over the TCP connection", Type: "string",
Type: "string", Enum: []any{"LF", "CR", "CRLF", "SLIP", "RAW"},
Enum: []any{"LF", "CR", "CRLF", "SLIP", "RAW"},
}, },
}, },
Required: []string{"port", "framing"}, Required: []string{"port", "framing"},
AdditionalProperties: &jsonschema.Schema{Not: &jsonschema.Schema{}}, AdditionalProperties: nil,
}, },
New: func(moduleConfig config.ModuleConfig) (common.Module, error) { New: func(moduleConfig config.ModuleConfig) (common.Module, error) {
params := moduleConfig.Params params := moduleConfig.Params
@@ -60,14 +73,12 @@ func init() {
return nil, fmt.Errorf("net.tcp.server framing error: %w", err) return nil, fmt.Errorf("net.tcp.server framing error: %w", err)
} }
inFramer := framer.GetFramer(framingMethodString) framer := framer.GetFramer(framingMethodString)
if inFramer == nil { if framer == nil {
return nil, fmt.Errorf("net.tcp.server unknown framing method: %s", framingMethodString) return nil, fmt.Errorf("net.tcp.server unknown framing method: %s", framingMethodString)
} }
outFramer := framer.GetFramer(framingMethodString)
ipString, err := params.GetString("ip") ipString, err := params.GetString("ip")
if err != nil { if err != nil {
if errors.Is(err, config.ErrParamNotFound) { if errors.Is(err, config.ErrParamNotFound) {
@@ -81,35 +92,11 @@ func init() {
if err != nil { if err != nil {
return nil, err return nil, err
} }
return &TCPServer{inFramer: inFramer, outFramer: outFramer, framerType: framingMethodString, Addr: addr, config: moduleConfig, logger: CreateLogger(moduleConfig)}, nil return &TCPServer{Framer: framer, Addr: addr, config: moduleConfig, quit: make(chan interface{}), logger: CreateLogger(moduleConfig)}, nil
}, },
}) })
} }
type tcpConnection struct {
conn *net.TCPConn
framer framer.Framer
}
type TCPServer struct {
config config.ModuleConfig
Addr *net.TCPAddr
inFramer framer.Framer
outFramer framer.Framer
framerType string
ctx context.Context
inputHandler common.InputHandler
wg sync.WaitGroup
connections []tcpConnection
connectionsMu sync.RWMutex
logger *slog.Logger
cancel context.CancelFunc
listener *net.TCPListener
listenerMu sync.Mutex
connectionShutdownCtx context.Context
connectionShutdown context.CancelFunc
}
func (ts *TCPServer) Id() string { func (ts *TCPServer) Id() string {
return ts.config.Id return ts.config.Id
} }
@@ -120,44 +107,77 @@ func (ts *TCPServer) Type() string {
func (ts *TCPServer) handleClient(client *net.TCPConn) { func (ts *TCPServer) handleClient(client *net.TCPConn) {
ts.connectionsMu.Lock() ts.connectionsMu.Lock()
ts.connections = append(ts.connections, tcpConnection{conn: client, framer: framer.GetFramer(ts.framerType)}) ts.connections = append(ts.connections, client)
ts.connectionsMu.Unlock() ts.connectionsMu.Unlock()
ts.logger.Debug("connection accepted", "remoteAddr", client.RemoteAddr().String()) ts.logger.Debug("net.tcp.server connection accepted", "remoteAddr", client.RemoteAddr().String())
defer func() { defer client.Close()
client.Close()
ts.connectionsMu.Lock()
for i := 0; i < len(ts.connections); i++ {
if ts.connections[i].conn == client {
ts.connections = slices.Delete(ts.connections, i, i+1)
break
}
}
ts.connectionsMu.Unlock()
ts.logger.Debug("connection closed", "remoteAddr", client.RemoteAddr().String())
}()
buffer := make([]byte, 1024) buffer := make([]byte, 1024)
for ts.ctx.Err() == nil && ts.connectionShutdownCtx.Err() == nil { ClientRead:
client.SetDeadline(time.Now().Add(time.Millisecond * 200)) for {
byteCount, err := client.Read(buffer) select {
if err != nil { case <-ts.quit:
opErr, ok := err.(*net.OpError) client.Close()
if ok { ts.connectionsMu.Lock()
//NOTE(jwetzell) we hit deadline for i := 0; i < len(ts.connections); i++ {
if opErr.Timeout() { if ts.connections[i] == client {
continue ts.connections = slices.Delete(ts.connections, i, i+1)
break
} }
} }
break ts.connectionsMu.Unlock()
} return
if ts.inFramer != nil { default:
if byteCount > 0 { client.SetDeadline(time.Now().Add(time.Millisecond * 200))
messages := ts.inFramer.Decode(buffer[0:byteCount]) byteCount, err := client.Read(buffer)
for _, message := range messages {
if ts.inputHandler != nil { if err != nil {
ts.inputHandler(ts.ctx, ts.Id(), message) if opErr, ok := err.(*net.OpError); ok {
} else { //NOTE(jwetzell) we hit deadline
ts.logger.Error("input received but no input handler is configured") if opErr.Timeout() {
continue ClientRead
}
if errors.Is(opErr, syscall.ECONNRESET) {
ts.connectionsMu.Lock()
for i := 0; i < len(ts.connections); i++ {
if ts.connections[i] == client {
ts.connections = slices.Delete(ts.connections, i, i+1)
break
}
}
ts.logger.Debug("connection reset", "remoteAddr", client.RemoteAddr().String())
ts.connectionsMu.Unlock()
}
}
if err.Error() == "EOF" {
ts.connectionsMu.Lock()
for i := 0; i < len(ts.connections); i++ {
if ts.connections[i] == client {
ts.connections = slices.Delete(ts.connections, i, i+1)
break
}
}
ts.logger.Debug("stream ended", "remoteAddr", client.RemoteAddr().String())
ts.connectionsMu.Unlock()
}
return
}
if ts.Framer != nil {
if byteCount > 0 {
messages := ts.Framer.Decode(buffer[0:byteCount])
for _, message := range messages {
if ts.router != nil {
senderAddr, ok := client.RemoteAddr().(*net.TCPAddr)
if ok {
senderCtx := context.WithValue(ts.ctx, common.SenderContextKey, senderAddr)
ts.router.HandleInput(senderCtx, ts.Id(), message)
} else {
ts.router.HandleInput(ts.ctx, ts.Id(), message)
}
} else {
ts.logger.Error("input received but no router is configured")
}
} }
} }
} }
@@ -165,48 +185,51 @@ func (ts *TCPServer) handleClient(client *net.TCPConn) {
} }
} }
func (ts *TCPServer) Start(ctx context.Context, inputHandler common.InputHandler) error { func (ts *TCPServer) Start(ctx context.Context) error {
ts.logger.Debug("running") ts.logger.Debug("running")
ts.inputHandler = inputHandler router, ok := ctx.Value(common.RouterContextKey).(common.RouteIO)
if !ok {
return errors.New("net.tcp.server unable to get router from context")
}
ts.router = router
moduleContext, cancel := context.WithCancel(ctx) moduleContext, cancel := context.WithCancel(ctx)
ts.ctx = moduleContext ts.ctx = moduleContext
ts.cancel = cancel ts.cancel = cancel
shutdownCtx, shutdownCancel := context.WithCancel(context.Background())
ts.connectionShutdownCtx = shutdownCtx
ts.connectionShutdown = shutdownCancel
listener, err := net.ListenTCP("tcp", ts.Addr) listener, err := net.ListenTCP("tcp", ts.Addr)
if err != nil { if err != nil {
return err return err
} }
ts.listenerMu.Lock()
ts.listener = listener
ts.listenerMu.Unlock()
ts.wg.Add(1) ts.wg.Add(1)
go func() {
<-ts.ctx.Done()
close(ts.quit)
listener.Close()
ts.logger.Debug("done")
}()
AcceptLoop: AcceptLoop:
for ts.ctx.Err() == nil { for {
conn, err := listener.AcceptTCP() conn, err := listener.AcceptTCP()
if err != nil { if err != nil {
if errors.Is(err, net.ErrClosed) {
break AcceptLoop
}
select { select {
case <-ts.ctx.Done(): case <-ts.quit:
break AcceptLoop break AcceptLoop
default: default:
ts.logger.Debug("problem with listener", "error", err) ts.logger.Debug("problem with listener", "error", err)
} }
} else { } else {
ts.wg.Go(func() { ts.wg.Add(1)
go func() {
ts.handleClient(conn) ts.handleClient(conn)
}) ts.wg.Done()
}()
} }
} }
ts.wg.Done() ts.wg.Done()
<-ts.ctx.Done() ts.wg.Wait()
ts.logger.Debug("done")
return nil return nil
} }
@@ -217,42 +240,23 @@ func (ts *TCPServer) Output(ctx context.Context, payload any) error {
return errors.New("net.tcp.server is only able to output bytes") return errors.New("net.tcp.server is only able to output bytes")
} }
ts.connectionsMu.Lock() ts.connectionsMu.Lock()
defer ts.connectionsMu.Unlock() errorString := ""
var errorString strings.Builder
if ts.outFramer == nil {
return errors.New("no output framer configured")
}
outputBytes := ts.outFramer.Encode(payloadBytes)
for _, connection := range ts.connections { for _, connection := range ts.connections {
_, err := connection.conn.Write(outputBytes) _, err := connection.Write(payloadBytes)
if err != nil { if err != nil {
fmt.Fprintf(&errorString, "%s\n", err.Error()) errorString += fmt.Sprintf("%s\n", err.Error())
} }
} }
ts.connectionsMu.Unlock()
if errorString.String() == "" { if errorString == "" {
return nil return nil
} }
return fmt.Errorf("net.tcp.server error during output: %s", errorString.String()) return fmt.Errorf("net.tcp.server error during output: %s", errorString)
} }
func (ts *TCPServer) Stop() { func (ts *TCPServer) Stop() {
if ts.cancel != nil { ts.cancel()
defer ts.cancel()
}
if ts.connectionShutdown != nil {
ts.connectionShutdown()
}
ts.listenerMu.Lock()
defer ts.listenerMu.Unlock()
if ts.listener != nil {
ts.listener.Close()
}
ts.logger.Debug("waiting for connections to close")
ts.wg.Wait() ts.wg.Wait()
ts.logger.Debug("all connections closed")
} }
+3 -55
View File
@@ -2,14 +2,13 @@ package module_test
import ( import (
"testing" "testing"
"time"
"github.com/jwetzell/showbridge-go/internal/config" "github.com/jwetzell/showbridge-go/internal/config"
"github.com/jwetzell/showbridge-go/internal/module" "github.com/jwetzell/showbridge-go/internal/module"
) )
func TestDbSqliteFromRegistry(t *testing.T) { func TestDbSqliteFromRegistry(t *testing.T) {
registration, ok := module.GetModuleRegistration("db.sqlite") registration, ok := module.ModuleRegistry["db.sqlite"]
if !ok { if !ok {
t.Fatalf("db.sqlite module not registered") t.Fatalf("db.sqlite module not registered")
} }
@@ -35,57 +34,6 @@ func TestDbSqliteFromRegistry(t *testing.T) {
} }
} }
func TestGoodDbSqlite(t *testing.T) {
testCases := []struct {
name string
params map[string]any
}{
{
name: "in memory db",
params: map[string]any{
"dsn": ":memory:",
},
},
{
name: "file db",
params: map[string]any{
"dsn": "test.db",
},
},
}
for _, test := range testCases {
t.Run(test.name, func(t *testing.T) {
registration, ok := module.GetModuleRegistration("db.sqlite")
if !ok {
t.Fatalf("db.sqlite module not registered")
}
moduleInstance, err := registration.New(config.ModuleConfig{
Id: "test",
Type: "db.sqlite",
Params: test.params,
})
if err != nil {
t.Fatalf("db.sqlite failed to create module: %s", err)
}
// TODO(jwetzell) this is kind of hacky
go func() {
time.Sleep(1 * time.Second)
moduleInstance.Stop()
}()
err = moduleInstance.Start(t.Context(), nil)
if err != nil {
t.Fatalf("db.sqlite failed to start: %s", err)
}
})
}
}
func TestBadDbSqlite(t *testing.T) { func TestBadDbSqlite(t *testing.T) {
tests := []struct { tests := []struct {
name string name string
@@ -107,7 +55,7 @@ func TestBadDbSqlite(t *testing.T) {
for _, test := range tests { for _, test := range tests {
t.Run(test.name, func(t *testing.T) { t.Run(test.name, func(t *testing.T) {
registration, ok := module.GetModuleRegistration("db.sqlite") registration, ok := module.ModuleRegistry["db.sqlite"]
if !ok { if !ok {
t.Fatalf("db.sqlite module not registered") t.Fatalf("db.sqlite module not registered")
} }
@@ -125,7 +73,7 @@ func TestBadDbSqlite(t *testing.T) {
return return
} }
err = moduleInstance.Start(t.Context(), nil) err = moduleInstance.Start(t.Context())
if err == nil { if err == nil {
t.Fatalf("db.sqlite expected to fail") t.Fatalf("db.sqlite expected to fail")
+3 -50
View File
@@ -2,14 +2,13 @@ package module_test
import ( import (
"testing" "testing"
"time"
"github.com/jwetzell/showbridge-go/internal/config" "github.com/jwetzell/showbridge-go/internal/config"
"github.com/jwetzell/showbridge-go/internal/module" "github.com/jwetzell/showbridge-go/internal/module"
) )
func TestHTTPServerFromRegistry(t *testing.T) { func TestHTTPServerFromRegistry(t *testing.T) {
registration, ok := module.GetModuleRegistration("http.server") registration, ok := module.ModuleRegistry["http.server"]
if !ok { if !ok {
t.Fatalf("http.server module not registered") t.Fatalf("http.server module not registered")
} }
@@ -35,52 +34,6 @@ func TestHTTPServerFromRegistry(t *testing.T) {
} }
} }
func TestGoodHTTPServer(t *testing.T) {
testCases := []struct {
name string
params map[string]any
}{
{
name: "minimal config",
params: map[string]any{
"port": 8000,
},
},
}
for _, test := range testCases {
t.Run(test.name, func(t *testing.T) {
registration, ok := module.GetModuleRegistration("http.server")
if !ok {
t.Fatalf("http.server module not registered")
}
moduleInstance, err := registration.New(config.ModuleConfig{
Id: "test",
Type: "http.server",
Params: test.params,
})
if err != nil {
t.Fatalf("http.server failed to create module: %s", err)
}
// TODO(jwetzell) this is kind of hacky
go func() {
time.Sleep(1 * time.Second)
moduleInstance.Stop()
}()
err = moduleInstance.Start(t.Context(), nil)
if err != nil {
t.Fatalf("http.server failed to start: %s", err)
}
})
}
}
func TestBadHTTPServer(t *testing.T) { func TestBadHTTPServer(t *testing.T) {
tests := []struct { tests := []struct {
name string name string
@@ -102,7 +55,7 @@ func TestBadHTTPServer(t *testing.T) {
for _, test := range tests { for _, test := range tests {
t.Run(test.name, func(t *testing.T) { t.Run(test.name, func(t *testing.T) {
registration, ok := module.GetModuleRegistration("http.server") registration, ok := module.ModuleRegistry["http.server"]
if !ok { if !ok {
t.Fatalf("http.server module not registered") t.Fatalf("http.server module not registered")
} }
@@ -120,7 +73,7 @@ func TestBadHTTPServer(t *testing.T) {
return return
} }
err = moduleInstance.Start(t.Context(), nil) err = moduleInstance.Start(t.Context())
if err == nil { if err == nil {
t.Fatalf("http.server expected to fail") t.Fatalf("http.server expected to fail")
+3 -3
View File
@@ -8,7 +8,7 @@ import (
) )
func TestMIDIInputFromRegistry(t *testing.T) { func TestMIDIInputFromRegistry(t *testing.T) {
registration, ok := module.GetModuleRegistration("midi.input") registration, ok := module.ModuleRegistry["midi.input"]
if !ok { if !ok {
t.Fatalf("midi.input module not registered") t.Fatalf("midi.input module not registered")
} }
@@ -55,7 +55,7 @@ func TestBadMIDIInput(t *testing.T) {
for _, test := range tests { for _, test := range tests {
t.Run(test.name, func(t *testing.T) { t.Run(test.name, func(t *testing.T) {
registration, ok := module.GetModuleRegistration("midi.input") registration, ok := module.ModuleRegistry["midi.input"]
if !ok { if !ok {
t.Fatalf("midi.input module not registered") t.Fatalf("midi.input module not registered")
} }
@@ -73,7 +73,7 @@ func TestBadMIDIInput(t *testing.T) {
return return
} }
err = moduleInstance.Start(t.Context(), nil) err = moduleInstance.Start(t.Context())
if err == nil { if err == nil {
t.Fatalf("midi.input expected to fail") t.Fatalf("midi.input expected to fail")
+3 -3
View File
@@ -8,7 +8,7 @@ import (
) )
func TestMIDIOutputFromRegistry(t *testing.T) { func TestMIDIOutputFromRegistry(t *testing.T) {
registration, ok := module.GetModuleRegistration("midi.output") registration, ok := module.ModuleRegistry["midi.output"]
if !ok { if !ok {
t.Fatalf("midi.output module not registered") t.Fatalf("midi.output module not registered")
} }
@@ -55,7 +55,7 @@ func TestBadMIDIOutput(t *testing.T) {
for _, test := range tests { for _, test := range tests {
t.Run(test.name, func(t *testing.T) { t.Run(test.name, func(t *testing.T) {
registration, ok := module.GetModuleRegistration("midi.output") registration, ok := module.ModuleRegistry["midi.output"]
if !ok { if !ok {
t.Fatalf("midi.output module not registered") t.Fatalf("midi.output module not registered")
} }
@@ -73,7 +73,7 @@ func TestBadMIDIOutput(t *testing.T) {
return return
} }
err = moduleInstance.Start(t.Context(), nil) err = moduleInstance.Start(t.Context())
if err == nil { if err == nil {
t.Fatalf("midi.output expected to fail") t.Fatalf("midi.output expected to fail")
+6 -9
View File
@@ -11,8 +11,7 @@ import (
func TestModuleBadRegistrationNoType(t *testing.T) { func TestModuleBadRegistrationNoType(t *testing.T) {
defer func() { defer func() {
r := recover() if r := recover(); r == nil {
if r == nil {
t.Fatalf("module registration should have panicked but did not") t.Fatalf("module registration should have panicked but did not")
} }
}() }()
@@ -20,15 +19,14 @@ func TestModuleBadRegistrationNoType(t *testing.T) {
module.RegisterModule(module.ModuleRegistration{ module.RegisterModule(module.ModuleRegistration{
Type: "", Type: "",
New: func(config config.ModuleConfig) (common.Module, error) { New: func(config config.ModuleConfig) (common.Module, error) {
return test.NewTestModule("test"), nil return &test.TestModule{}, nil
}, },
}) })
} }
func TestModuleBadRegistrationNoNew(t *testing.T) { func TestModuleBadRegistrationNoNew(t *testing.T) {
defer func() { defer func() {
r := recover() if r := recover(); r == nil {
if r == nil {
t.Fatalf("processor registration should have panicked but did not") t.Fatalf("processor registration should have panicked but did not")
} }
}() }()
@@ -41,8 +39,7 @@ func TestModuleBadRegistrationNoNew(t *testing.T) {
func TestModuleBadRegistrationExistingType(t *testing.T) { func TestModuleBadRegistrationExistingType(t *testing.T) {
defer func() { defer func() {
r := recover() if r := recover(); r == nil {
if r == nil {
t.Fatalf("processor registration should have panicked but did not") t.Fatalf("processor registration should have panicked but did not")
} }
}() }()
@@ -50,14 +47,14 @@ func TestModuleBadRegistrationExistingType(t *testing.T) {
module.RegisterModule(module.ModuleRegistration{ module.RegisterModule(module.ModuleRegistration{
Type: "module.test", Type: "module.test",
New: func(config config.ModuleConfig) (common.Module, error) { New: func(config config.ModuleConfig) (common.Module, error) {
return test.NewTestModule("test"), nil return &test.TestModule{}, nil
}, },
}) })
module.RegisterModule(module.ModuleRegistration{ module.RegisterModule(module.ModuleRegistration{
Type: "module.test", Type: "module.test",
New: func(config config.ModuleConfig) (common.Module, error) { New: func(config config.ModuleConfig) (common.Module, error) {
return test.NewTestModule("test"), nil return &test.TestModule{}, nil
}, },
}) })
} }
+3 -3
View File
@@ -8,7 +8,7 @@ import (
) )
func TestMQTTClientFromRegistry(t *testing.T) { func TestMQTTClientFromRegistry(t *testing.T) {
registration, ok := module.GetModuleRegistration("mqtt.client") registration, ok := module.ModuleRegistry["mqtt.client"]
if !ok { if !ok {
t.Fatalf("mqtt.client module not registered") t.Fatalf("mqtt.client module not registered")
} }
@@ -98,7 +98,7 @@ func TestBadMQTTClient(t *testing.T) {
for _, test := range tests { for _, test := range tests {
t.Run(test.name, func(t *testing.T) { t.Run(test.name, func(t *testing.T) {
registration, ok := module.GetModuleRegistration("mqtt.client") registration, ok := module.ModuleRegistry["mqtt.client"]
if !ok { if !ok {
t.Fatalf("mqtt.client module not registered") t.Fatalf("mqtt.client module not registered")
} }
@@ -116,7 +116,7 @@ func TestBadMQTTClient(t *testing.T) {
return return
} }
err = moduleInstance.Start(t.Context(), nil) err = moduleInstance.Start(t.Context())
if err == nil { if err == nil {
t.Fatalf("mqtt.client expected to fail") t.Fatalf("mqtt.client expected to fail")
+3 -3
View File
@@ -8,7 +8,7 @@ import (
) )
func TestNATSClientFromRegistry(t *testing.T) { func TestNATSClientFromRegistry(t *testing.T) {
registration, ok := module.GetModuleRegistration("nats.client") registration, ok := module.ModuleRegistry["nats.client"]
if !ok { if !ok {
t.Fatalf("nats.client module not registered") t.Fatalf("nats.client module not registered")
} }
@@ -76,7 +76,7 @@ func TestBadNATSClient(t *testing.T) {
for _, test := range tests { for _, test := range tests {
t.Run(test.name, func(t *testing.T) { t.Run(test.name, func(t *testing.T) {
registration, ok := module.GetModuleRegistration("nats.client") registration, ok := module.ModuleRegistry["nats.client"]
if !ok { if !ok {
t.Fatalf("nats.client module not registered") t.Fatalf("nats.client module not registered")
} }
@@ -94,7 +94,7 @@ func TestBadNATSClient(t *testing.T) {
return return
} }
err = moduleInstance.Start(t.Context(), nil) err = moduleInstance.Start(t.Context())
if err == nil { if err == nil {
t.Fatalf("nats.client expected to fail") t.Fatalf("nats.client expected to fail")
+3 -50
View File
@@ -2,14 +2,13 @@ package module_test
import ( import (
"testing" "testing"
"time"
"github.com/jwetzell/showbridge-go/internal/config" "github.com/jwetzell/showbridge-go/internal/config"
"github.com/jwetzell/showbridge-go/internal/module" "github.com/jwetzell/showbridge-go/internal/module"
) )
func TestNATSServerFromRegistry(t *testing.T) { func TestNATSServerFromRegistry(t *testing.T) {
registration, ok := module.GetModuleRegistration("nats.server") registration, ok := module.ModuleRegistry["nats.server"]
if !ok { if !ok {
t.Fatalf("nats.server module not registered") t.Fatalf("nats.server module not registered")
} }
@@ -36,52 +35,6 @@ func TestNATSServerFromRegistry(t *testing.T) {
} }
} }
func TestGoodNATSServer(t *testing.T) {
testCases := []struct {
name string
params map[string]any
}{
{
name: "minimal config",
params: map[string]any{
"port": 4222,
},
},
}
for _, test := range testCases {
t.Run(test.name, func(t *testing.T) {
registration, ok := module.GetModuleRegistration("nats.server")
if !ok {
t.Fatalf("nats.server module not registered")
}
moduleInstance, err := registration.New(config.ModuleConfig{
Id: "test",
Type: "nats.server",
Params: test.params,
})
if err != nil {
t.Fatalf("nats.server failed to create module: %s", err)
}
// TODO(jwetzell) this is kind of hacky
go func() {
time.Sleep(1 * time.Second)
moduleInstance.Stop()
}()
err = moduleInstance.Start(t.Context(), nil)
if err != nil {
t.Fatalf("nats.server failed to start: %s", err)
}
})
}
}
func TestBadNATSServer(t *testing.T) { func TestBadNATSServer(t *testing.T) {
tests := []struct { tests := []struct {
name string name string
@@ -100,7 +53,7 @@ func TestBadNATSServer(t *testing.T) {
for _, test := range tests { for _, test := range tests {
t.Run(test.name, func(t *testing.T) { t.Run(test.name, func(t *testing.T) {
registration, ok := module.GetModuleRegistration("nats.server") registration, ok := module.ModuleRegistry["nats.server"]
if !ok { if !ok {
t.Fatalf("nats.server module not registered") t.Fatalf("nats.server module not registered")
} }
@@ -118,7 +71,7 @@ func TestBadNATSServer(t *testing.T) {
return return
} }
err = moduleInstance.Start(t.Context(), nil) err = moduleInstance.Start(t.Context())
if err == nil { if err == nil {
t.Fatalf("nats.server expected to fail") t.Fatalf("nats.server expected to fail")
+3 -3
View File
@@ -8,7 +8,7 @@ import (
) )
func TestPSNClientFromRegistry(t *testing.T) { func TestPSNClientFromRegistry(t *testing.T) {
registration, ok := module.GetModuleRegistration("psn.client") registration, ok := module.ModuleRegistry["psn.client"]
if !ok { if !ok {
t.Fatalf("psn.client module not registered") t.Fatalf("psn.client module not registered")
} }
@@ -41,7 +41,7 @@ func TestBadPSNClient(t *testing.T) {
for _, test := range tests { for _, test := range tests {
t.Run(test.name, func(t *testing.T) { t.Run(test.name, func(t *testing.T) {
registration, ok := module.GetModuleRegistration("psn.client") registration, ok := module.ModuleRegistry["psn.client"]
if !ok { if !ok {
t.Fatalf("psn.client module not registered") t.Fatalf("psn.client module not registered")
} }
@@ -59,7 +59,7 @@ func TestBadPSNClient(t *testing.T) {
return return
} }
err = moduleInstance.Start(t.Context(), nil) err = moduleInstance.Start(t.Context())
if err == nil { if err == nil {
t.Fatalf("psn.client expected to fail") t.Fatalf("psn.client expected to fail")
+3 -3
View File
@@ -8,7 +8,7 @@ import (
) )
func TestRedisClientFromRegistry(t *testing.T) { func TestRedisClientFromRegistry(t *testing.T) {
registration, ok := module.GetModuleRegistration("redis.client") registration, ok := module.ModuleRegistry["redis.client"]
if !ok { if !ok {
t.Fatalf("redis.client module not registered") t.Fatalf("redis.client module not registered")
} }
@@ -76,7 +76,7 @@ func TestBadRedisClient(t *testing.T) {
for _, test := range tests { for _, test := range tests {
t.Run(test.name, func(t *testing.T) { t.Run(test.name, func(t *testing.T) {
registration, ok := module.GetModuleRegistration("redis.client") registration, ok := module.ModuleRegistry["redis.client"]
if !ok { if !ok {
t.Fatalf("redis.client module not registered") t.Fatalf("redis.client module not registered")
} }
@@ -94,7 +94,7 @@ func TestBadRedisClient(t *testing.T) {
return return
} }
err = moduleInstance.Start(t.Context(), nil) err = moduleInstance.Start(t.Context())
if err == nil { if err == nil {
t.Fatalf("redis.client expected to fail") t.Fatalf("redis.client expected to fail")
+3 -3
View File
@@ -8,7 +8,7 @@ import (
) )
func TestSerialClientFromRegistry(t *testing.T) { func TestSerialClientFromRegistry(t *testing.T) {
registration, ok := module.GetModuleRegistration("serial.client") registration, ok := module.ModuleRegistry["serial.client"]
if !ok { if !ok {
t.Fatalf("serial.client module not registered") t.Fatalf("serial.client module not registered")
} }
@@ -85,7 +85,7 @@ func TestBadSerialClient(t *testing.T) {
for _, test := range tests { for _, test := range tests {
t.Run(test.name, func(t *testing.T) { t.Run(test.name, func(t *testing.T) {
registration, ok := module.GetModuleRegistration("serial.client") registration, ok := module.ModuleRegistry["serial.client"]
if !ok { if !ok {
t.Fatalf("serial.client module not registered") t.Fatalf("serial.client module not registered")
} }
@@ -103,7 +103,7 @@ func TestBadSerialClient(t *testing.T) {
return return
} }
err = moduleInstance.Start(t.Context(), nil) err = moduleInstance.Start(t.Context())
if err == nil { if err == nil {
t.Fatalf("serial.client expected to fail") t.Fatalf("serial.client expected to fail")
+3 -3
View File
@@ -8,7 +8,7 @@ import (
) )
func TestSIPCallServerFromRegistry(t *testing.T) { func TestSIPCallServerFromRegistry(t *testing.T) {
registration, ok := module.GetModuleRegistration("sip.call.server") registration, ok := module.ModuleRegistry["sip.call.server"]
if !ok { if !ok {
t.Fatalf("sip.call.server module not registered") t.Fatalf("sip.call.server module not registered")
} }
@@ -70,7 +70,7 @@ func TestBadSIPCallServer(t *testing.T) {
for _, test := range tests { for _, test := range tests {
t.Run(test.name, func(t *testing.T) { t.Run(test.name, func(t *testing.T) {
registration, ok := module.GetModuleRegistration("sip.call.server") registration, ok := module.ModuleRegistry["sip.call.server"]
if !ok { if !ok {
t.Fatalf("sip.call.server module not registered") t.Fatalf("sip.call.server module not registered")
} }
@@ -88,7 +88,7 @@ func TestBadSIPCallServer(t *testing.T) {
return return
} }
err = moduleInstance.Start(t.Context(), nil) err = moduleInstance.Start(t.Context())
if err == nil { if err == nil {
t.Fatalf("sip.call.server expected to fail") t.Fatalf("sip.call.server expected to fail")
+3 -3
View File
@@ -8,7 +8,7 @@ import (
) )
func TestSIPDTMFServerFromRegistry(t *testing.T) { func TestSIPDTMFServerFromRegistry(t *testing.T) {
registration, ok := module.GetModuleRegistration("sip.dtmf.server") registration, ok := module.ModuleRegistry["sip.dtmf.server"]
if !ok { if !ok {
t.Fatalf("sip.dtmf.server module not registered") t.Fatalf("sip.dtmf.server module not registered")
} }
@@ -89,7 +89,7 @@ func TestBadSIPDTMFServer(t *testing.T) {
for _, test := range tests { for _, test := range tests {
t.Run(test.name, func(t *testing.T) { t.Run(test.name, func(t *testing.T) {
registration, ok := module.GetModuleRegistration("sip.dtmf.server") registration, ok := module.ModuleRegistry["sip.dtmf.server"]
if !ok { if !ok {
t.Fatalf("sip.dtmf.server module not registered") t.Fatalf("sip.dtmf.server module not registered")
} }
@@ -107,7 +107,7 @@ func TestBadSIPDTMFServer(t *testing.T) {
return return
} }
err = moduleInstance.Start(t.Context(), nil) err = moduleInstance.Start(t.Context())
if err == nil { if err == nil {
t.Fatalf("sip.dtmf.server expected to fail") t.Fatalf("sip.dtmf.server expected to fail")
+3 -3
View File
@@ -8,7 +8,7 @@ import (
) )
func TestTCPClientFromRegistry(t *testing.T) { func TestTCPClientFromRegistry(t *testing.T) {
registration, ok := module.GetModuleRegistration("net.tcp.client") registration, ok := module.ModuleRegistry["net.tcp.client"]
if !ok { if !ok {
t.Fatalf("net.tcp.client module not registered") t.Fatalf("net.tcp.client module not registered")
} }
@@ -77,7 +77,7 @@ func TestBadTCPClient(t *testing.T) {
for _, test := range tests { for _, test := range tests {
t.Run(test.name, func(t *testing.T) { t.Run(test.name, func(t *testing.T) {
registration, ok := module.GetModuleRegistration("net.tcp.client") registration, ok := module.ModuleRegistry["net.tcp.client"]
if !ok { if !ok {
t.Fatalf("net.tcp.client module not registered") t.Fatalf("net.tcp.client module not registered")
} }
@@ -95,7 +95,7 @@ func TestBadTCPClient(t *testing.T) {
return return
} }
err = moduleInstance.Start(t.Context(), nil) err = moduleInstance.Start(t.Context())
if err == nil { if err == nil {
t.Fatalf("net.tcp.client expected to fail") t.Fatalf("net.tcp.client expected to fail")
+3 -50
View File
@@ -2,14 +2,13 @@ package module_test
import ( import (
"testing" "testing"
"time"
"github.com/jwetzell/showbridge-go/internal/config" "github.com/jwetzell/showbridge-go/internal/config"
"github.com/jwetzell/showbridge-go/internal/module" "github.com/jwetzell/showbridge-go/internal/module"
) )
func TestTCPServerFromRegistry(t *testing.T) { func TestTCPServerFromRegistry(t *testing.T) {
registration, ok := module.GetModuleRegistration("net.tcp.server") registration, ok := module.ModuleRegistry["net.tcp.server"]
if !ok { if !ok {
t.Fatalf("net.tcp.server module not registered") t.Fatalf("net.tcp.server module not registered")
} }
@@ -36,52 +35,6 @@ func TestTCPServerFromRegistry(t *testing.T) {
} }
} }
func TestGoodTCPServer(t *testing.T) {
testCases := []struct {
name string
params map[string]any
}{
{
name: "minimal config",
params: map[string]any{
"port": 8000,
"framing": "LF",
},
},
}
for _, test := range testCases {
t.Run(test.name, func(t *testing.T) {
registration, ok := module.GetModuleRegistration("net.tcp.server")
if !ok {
t.Fatalf("net.tcp.server module not registered")
}
moduleInstance, err := registration.New(config.ModuleConfig{
Id: "test",
Type: "net.tcp.server",
Params: test.params,
})
if err != nil {
t.Fatalf("net.tcp.server failed to create module: %s", err)
}
// TODO(jwetzell) this is kind of hacky
go func() {
time.Sleep(1 * time.Second)
moduleInstance.Stop()
}()
err = moduleInstance.Start(t.Context(), nil)
if err != nil {
t.Fatalf("net.tcp.server failed to start: %s", err)
}
})
}
}
func TestBadTCPServer(t *testing.T) { func TestBadTCPServer(t *testing.T) {
tests := []struct { tests := []struct {
name string name string
@@ -149,7 +102,7 @@ func TestBadTCPServer(t *testing.T) {
for _, test := range tests { for _, test := range tests {
t.Run(test.name, func(t *testing.T) { t.Run(test.name, func(t *testing.T) {
registration, ok := module.GetModuleRegistration("net.tcp.server") registration, ok := module.ModuleRegistry["net.tcp.server"]
if !ok { if !ok {
t.Fatalf("net.tcp.server module not registered") t.Fatalf("net.tcp.server module not registered")
} }
@@ -167,7 +120,7 @@ func TestBadTCPServer(t *testing.T) {
return return
} }
err = moduleInstance.Start(t.Context(), nil) err = moduleInstance.Start(t.Context())
if err == nil { if err == nil {
t.Fatalf("net.tcp.server expected to fail") t.Fatalf("net.tcp.server expected to fail")
Binary file not shown.
+3 -49
View File
@@ -2,14 +2,13 @@ package module_test
import ( import (
"testing" "testing"
"time"
"github.com/jwetzell/showbridge-go/internal/config" "github.com/jwetzell/showbridge-go/internal/config"
"github.com/jwetzell/showbridge-go/internal/module" "github.com/jwetzell/showbridge-go/internal/module"
) )
func TestTimeIntervalFromRegistry(t *testing.T) { func TestTimeIntervalFromRegistry(t *testing.T) {
registration, ok := module.GetModuleRegistration("time.interval") registration, ok := module.ModuleRegistry["time.interval"]
if !ok { if !ok {
t.Fatalf("time.interval module not registered") t.Fatalf("time.interval module not registered")
} }
@@ -35,51 +34,6 @@ func TestTimeIntervalFromRegistry(t *testing.T) {
} }
} }
func TestGoodTimeInterval(t *testing.T) {
testCases := []struct {
name string
params map[string]any
}{
{
name: "minimal config",
params: map[string]any{
"duration": 1000,
},
},
}
for _, test := range testCases {
t.Run(test.name, func(t *testing.T) {
registration, ok := module.GetModuleRegistration("time.interval")
if !ok {
t.Fatalf("time.interval module not registered")
}
moduleInstance, err := registration.New(config.ModuleConfig{
Id: "test",
Type: "time.interval",
Params: test.params,
})
if err != nil {
t.Fatalf("time.interval failed to create module: %s", err)
}
// TODO(jwetzell) this is kind of hacky
go func() {
time.Sleep(1 * time.Second)
moduleInstance.Stop()
}()
err = moduleInstance.Start(t.Context(), nil)
if err != nil {
t.Fatalf("time.interval failed to start: %s", err)
}
})
}
}
func TestBadTimeInterval(t *testing.T) { func TestBadTimeInterval(t *testing.T) {
tests := []struct { tests := []struct {
name string name string
@@ -103,7 +57,7 @@ func TestBadTimeInterval(t *testing.T) {
for _, test := range tests { for _, test := range tests {
t.Run(test.name, func(t *testing.T) { t.Run(test.name, func(t *testing.T) {
registration, ok := module.GetModuleRegistration("time.interval") registration, ok := module.ModuleRegistry["time.interval"]
if !ok { if !ok {
t.Fatalf("time.interval module not registered") t.Fatalf("time.interval module not registered")
} }
@@ -121,7 +75,7 @@ func TestBadTimeInterval(t *testing.T) {
return return
} }
err = moduleInstance.Start(t.Context(), nil) err = moduleInstance.Start(t.Context())
if err == nil { if err == nil {
t.Fatalf("time.interval expected to fail") t.Fatalf("time.interval expected to fail")
+3 -49
View File
@@ -2,14 +2,13 @@ package module_test
import ( import (
"testing" "testing"
"time"
"github.com/jwetzell/showbridge-go/internal/config" "github.com/jwetzell/showbridge-go/internal/config"
"github.com/jwetzell/showbridge-go/internal/module" "github.com/jwetzell/showbridge-go/internal/module"
) )
func TestTimeTimerFromRegistry(t *testing.T) { func TestTimeTimerFromRegistry(t *testing.T) {
registration, ok := module.GetModuleRegistration("time.timer") registration, ok := module.ModuleRegistry["time.timer"]
if !ok { if !ok {
t.Fatalf("time.timer module not registered") t.Fatalf("time.timer module not registered")
} }
@@ -35,51 +34,6 @@ func TestTimeTimerFromRegistry(t *testing.T) {
} }
} }
func TestGoodTimeTimer(t *testing.T) {
testCases := []struct {
name string
params map[string]any
}{
{
name: "minimal config",
params: map[string]any{
"duration": 2000,
},
},
}
for _, test := range testCases {
t.Run(test.name, func(t *testing.T) {
registration, ok := module.GetModuleRegistration("time.timer")
if !ok {
t.Fatalf("time.timer module not registered")
}
moduleInstance, err := registration.New(config.ModuleConfig{
Id: "test",
Type: "time.timer",
Params: test.params,
})
if err != nil {
t.Fatalf("time.timer failed to create module: %s", err)
}
// TODO(jwetzell) this is kind of hacky
go func() {
time.Sleep(1 * time.Second)
moduleInstance.Stop()
}()
err = moduleInstance.Start(t.Context(), nil)
if err != nil {
t.Fatalf("time.timer failed to start: %s", err)
}
})
}
}
func TestBadTimeTimer(t *testing.T) { func TestBadTimeTimer(t *testing.T) {
tests := []struct { tests := []struct {
name string name string
@@ -103,7 +57,7 @@ func TestBadTimeTimer(t *testing.T) {
for _, test := range tests { for _, test := range tests {
t.Run(test.name, func(t *testing.T) { t.Run(test.name, func(t *testing.T) {
registration, ok := module.GetModuleRegistration("time.timer") registration, ok := module.ModuleRegistry["time.timer"]
if !ok { if !ok {
t.Fatalf("time.timer module not registered") t.Fatalf("time.timer module not registered")
} }
@@ -121,7 +75,7 @@ func TestBadTimeTimer(t *testing.T) {
return return
} }
err = moduleInstance.Start(t.Context(), nil) err = moduleInstance.Start(t.Context())
if err == nil { if err == nil {
t.Fatalf("time.timer expected to fail") t.Fatalf("time.timer expected to fail")
+3 -51
View File
@@ -2,14 +2,13 @@ package module_test
import ( import (
"testing" "testing"
"time"
"github.com/jwetzell/showbridge-go/internal/config" "github.com/jwetzell/showbridge-go/internal/config"
"github.com/jwetzell/showbridge-go/internal/module" "github.com/jwetzell/showbridge-go/internal/module"
) )
func TestUDPClientFromRegistry(t *testing.T) { func TestUDPClientFromRegistry(t *testing.T) {
registration, ok := module.GetModuleRegistration("net.udp.client") registration, ok := module.ModuleRegistry["net.udp.client"]
if !ok { if !ok {
t.Fatalf("udp.client module not registered") t.Fatalf("udp.client module not registered")
} }
@@ -37,53 +36,6 @@ func TestUDPClientFromRegistry(t *testing.T) {
} }
} }
func TestGoodUDPClient(t *testing.T) {
testCases := []struct {
name string
params map[string]any
}{
{
name: "minimal config",
params: map[string]any{
"host": "localhost",
"port": 8000,
},
},
}
for _, test := range testCases {
t.Run(test.name, func(t *testing.T) {
registration, ok := module.GetModuleRegistration("net.udp.client")
if !ok {
t.Fatalf("net.udp.client module not registered")
}
moduleInstance, err := registration.New(config.ModuleConfig{
Id: "test",
Type: "net.udp.client",
Params: test.params,
})
if err != nil {
t.Fatalf("net.udp.client failed to create module: %s", err)
}
// TODO(jwetzell) this is kind of hacky
go func() {
time.Sleep(1 * time.Second)
moduleInstance.Stop()
}()
err = moduleInstance.Start(t.Context(), nil)
if err != nil {
t.Fatalf("net.udp.client failed to start: %s", err)
}
})
}
}
func TestBadUDPClient(t *testing.T) { func TestBadUDPClient(t *testing.T) {
tests := []struct { tests := []struct {
name string name string
@@ -125,7 +77,7 @@ func TestBadUDPClient(t *testing.T) {
for _, test := range tests { for _, test := range tests {
t.Run(test.name, func(t *testing.T) { t.Run(test.name, func(t *testing.T) {
registration, ok := module.GetModuleRegistration("net.udp.client") registration, ok := module.ModuleRegistry["net.udp.client"]
if !ok { if !ok {
t.Fatalf("net.udp.client module not registered") t.Fatalf("net.udp.client module not registered")
} }
@@ -143,7 +95,7 @@ func TestBadUDPClient(t *testing.T) {
return return
} }
err = moduleInstance.Start(t.Context(), nil) err = moduleInstance.Start(t.Context())
if err == nil { if err == nil {
t.Fatalf("net.udp.client expected to fail") t.Fatalf("net.udp.client expected to fail")
+3 -3
View File
@@ -8,7 +8,7 @@ import (
) )
func TestUDPMulticastFromRegistry(t *testing.T) { func TestUDPMulticastFromRegistry(t *testing.T) {
registration, ok := module.GetModuleRegistration("net.udp.multicast") registration, ok := module.ModuleRegistry["net.udp.multicast"]
if !ok { if !ok {
t.Fatalf("udp.multicast module not registered") t.Fatalf("udp.multicast module not registered")
} }
@@ -84,7 +84,7 @@ func TestBadUDPMulticast(t *testing.T) {
for _, test := range tests { for _, test := range tests {
t.Run(test.name, func(t *testing.T) { t.Run(test.name, func(t *testing.T) {
registration, ok := module.GetModuleRegistration("net.udp.multicast") registration, ok := module.ModuleRegistry["net.udp.multicast"]
if !ok { if !ok {
t.Fatalf("net.udp.multicast module not registered") t.Fatalf("net.udp.multicast module not registered")
} }
@@ -102,7 +102,7 @@ func TestBadUDPMulticast(t *testing.T) {
return return
} }
err = moduleInstance.Start(t.Context(), nil) err = moduleInstance.Start(t.Context())
if err == nil { if err == nil {
t.Fatalf("net.udp.multicast expected to fail") t.Fatalf("net.udp.multicast expected to fail")
+3 -50
View File
@@ -2,14 +2,13 @@ package module_test
import ( import (
"testing" "testing"
"time"
"github.com/jwetzell/showbridge-go/internal/config" "github.com/jwetzell/showbridge-go/internal/config"
"github.com/jwetzell/showbridge-go/internal/module" "github.com/jwetzell/showbridge-go/internal/module"
) )
func TestUDPServerFromRegistry(t *testing.T) { func TestUDPServerFromRegistry(t *testing.T) {
registration, ok := module.GetModuleRegistration("net.udp.server") registration, ok := module.ModuleRegistry["net.udp.server"]
if !ok { if !ok {
t.Fatalf("net.udp.server module not registered") t.Fatalf("net.udp.server module not registered")
} }
@@ -35,52 +34,6 @@ func TestUDPServerFromRegistry(t *testing.T) {
} }
} }
func TestGoodUDPServer(t *testing.T) {
testCases := []struct {
name string
params map[string]any
}{
{
name: "minimal config",
params: map[string]any{
"port": 8000,
},
},
}
for _, test := range testCases {
t.Run(test.name, func(t *testing.T) {
registration, ok := module.GetModuleRegistration("net.udp.server")
if !ok {
t.Fatalf("net.udp.server module not registered")
}
moduleInstance, err := registration.New(config.ModuleConfig{
Id: "test",
Type: "net.udp.server",
Params: test.params,
})
if err != nil {
t.Fatalf("net.udp.server failed to create module: %s", err)
}
// TODO(jwetzell) this is kind of hacky
go func() {
time.Sleep(1 * time.Second)
moduleInstance.Stop()
}()
err = moduleInstance.Start(t.Context(), nil)
if err != nil {
t.Fatalf("net.udp.server failed to start: %s", err)
}
})
}
}
func TestBadUDPServer(t *testing.T) { func TestBadUDPServer(t *testing.T) {
tests := []struct { tests := []struct {
name string name string
@@ -128,7 +81,7 @@ func TestBadUDPServer(t *testing.T) {
for _, test := range tests { for _, test := range tests {
t.Run(test.name, func(t *testing.T) { t.Run(test.name, func(t *testing.T) {
registration, ok := module.GetModuleRegistration("net.udp.server") registration, ok := module.ModuleRegistry["net.udp.server"]
if !ok { if !ok {
t.Fatalf("net.udp.server module not registered") t.Fatalf("net.udp.server module not registered")
} }
@@ -146,7 +99,7 @@ func TestBadUDPServer(t *testing.T) {
return return
} }
err = moduleInstance.Start(t.Context(), nil) err = moduleInstance.Start(t.Context())
if err == nil { if err == nil {
t.Fatalf("net.udp.server expected to fail") t.Fatalf("net.udp.server expected to fail")
@@ -1,96 +0,0 @@
package module_test
import (
"testing"
"github.com/jwetzell/showbridge-go/internal/config"
"github.com/jwetzell/showbridge-go/internal/module"
)
func TestWebSocketClientFromRegistry(t *testing.T) {
registration, ok := module.GetModuleRegistration("websocket.client")
if !ok {
t.Fatalf("websocket.client module not registered")
}
moduleInstance, err := registration.New(config.ModuleConfig{
Id: "test",
Type: "websocket.client",
Params: map[string]any{
"url": "ws://localhost",
},
})
if err != nil {
t.Fatalf("failed to create websocket.client module: %s", err)
}
if moduleInstance.Id() != "test" {
t.Fatalf("websocket.client module has wrong id: %s", moduleInstance.Id())
}
if moduleInstance.Type() != "websocket.client" {
t.Fatalf("websocket.client module has wrong type: %s", moduleInstance.Type())
}
}
func TestBadWebSocketClient(t *testing.T) {
tests := []struct {
name string
params map[string]any
errorString string
}{
{
name: "no url param",
params: map[string]any{},
errorString: "websocket.client url error: not found",
},
{
name: "non-string url param",
params: map[string]any{
"url": 123,
},
errorString: "websocket.client url error: not a string",
},
{
name: "invalid url param",
params: map[string]any{
"url": "invalid-url",
},
errorString: "websocket.client url error: scheme must be ws or wss",
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
registration, ok := module.GetModuleRegistration("websocket.client")
if !ok {
t.Fatalf("websocket.client module not registered")
}
moduleInstance, err := registration.New(config.ModuleConfig{
Id: "test",
Type: "websocket.client",
Params: test.params,
})
if err != nil {
if test.errorString != err.Error() {
t.Fatalf("websocket.client got error '%s', expected '%s'", err.Error(), test.errorString)
}
return
}
err = moduleInstance.Start(t.Context(), nil)
if err == nil {
t.Fatalf("websocket.client expected to fail")
}
if err.Error() != test.errorString {
t.Fatalf("websocket.client got error '%s', expected '%s'", err.Error(), test.errorString)
}
})
}
}
+29 -28
View File
@@ -2,6 +2,7 @@ package module
import ( import (
"context" "context"
"errors"
"fmt" "fmt"
"log/slog" "log/slog"
"time" "time"
@@ -11,6 +12,16 @@ import (
"github.com/jwetzell/showbridge-go/internal/config" "github.com/jwetzell/showbridge-go/internal/config"
) )
type TimeInterval struct {
config config.ModuleConfig
Duration uint32
ctx context.Context
router common.RouteIO
ticker *time.Ticker
logger *slog.Logger
cancel context.CancelFunc
}
func init() { func init() {
RegisterModule(ModuleRegistration{ RegisterModule(ModuleRegistration{
Type: "time.interval", Type: "time.interval",
@@ -20,12 +31,12 @@ func init() {
Properties: map[string]*jsonschema.Schema{ Properties: map[string]*jsonschema.Schema{
"duration": { "duration": {
Title: "Duration", Title: "Duration",
Description: "time in milliseconds between emitted events",
Type: "integer", Type: "integer",
Description: "Interval duration in milliseconds",
}, },
}, },
Required: []string{"duration"}, Required: []string{"duration"},
AdditionalProperties: &jsonschema.Schema{Not: &jsonschema.Schema{}}, AdditionalProperties: nil,
}, },
New: func(config config.ModuleConfig) (common.Module, error) { New: func(config config.ModuleConfig) (common.Module, error) {
params := config.Params params := config.Params
@@ -39,16 +50,6 @@ func init() {
}) })
} }
type TimeInterval struct {
config config.ModuleConfig
Duration uint32
ctx context.Context
inputHandler common.InputHandler
ticker *time.Ticker
logger *slog.Logger
cancel context.CancelFunc
}
func (i *TimeInterval) Id() string { func (i *TimeInterval) Id() string {
return i.config.Id return i.config.Id
} }
@@ -57,36 +58,36 @@ func (i *TimeInterval) Type() string {
return i.config.Type return i.config.Type
} }
func (i *TimeInterval) Start(ctx context.Context, inputHandler common.InputHandler) error { func (i *TimeInterval) Start(ctx context.Context) error {
i.logger.Debug("running") i.logger.Debug("running")
i.inputHandler = inputHandler router, ok := ctx.Value(common.RouterContextKey).(common.RouteIO)
if !ok {
return errors.New("time.interval unable to get router from context")
}
i.router = router
moduleContext, cancel := context.WithCancel(ctx) moduleContext, cancel := context.WithCancel(ctx)
i.ctx = moduleContext i.ctx = moduleContext
i.cancel = cancel i.cancel = cancel
ticker := time.NewTicker(time.Millisecond * time.Duration(i.Duration)) ticker := time.NewTicker(time.Millisecond * time.Duration(i.Duration))
i.ticker = ticker i.ticker = ticker
defer ticker.Stop()
for i.ctx.Err() == nil { for {
select { select {
case <-i.ctx.Done():
i.logger.Debug("done")
return nil
case <-ticker.C: case <-ticker.C:
if i.inputHandler != nil { if i.router != nil {
i.inputHandler(i.ctx, i.Id(), time.Now()) i.router.HandleInput(i.ctx, i.Id(), time.Now())
} }
default:
continue
} }
} }
<-i.ctx.Done()
i.logger.Debug("done")
return nil
} }
func (i *TimeInterval) Stop() { func (i *TimeInterval) Stop() {
if i.cancel != nil { i.cancel()
defer i.cancel()
}
if i.ticker != nil {
i.ticker.Stop()
}
} }
+27 -26
View File
@@ -2,6 +2,7 @@ package module
import ( import (
"context" "context"
"errors"
"fmt" "fmt"
"log/slog" "log/slog"
"time" "time"
@@ -11,6 +12,16 @@ import (
"github.com/jwetzell/showbridge-go/internal/config" "github.com/jwetzell/showbridge-go/internal/config"
) )
type TimeTimer struct {
config config.ModuleConfig
Duration uint32
ctx context.Context
router common.RouteIO
timer *time.Timer
logger *slog.Logger
cancel context.CancelFunc
}
func init() { func init() {
RegisterModule(ModuleRegistration{ RegisterModule(ModuleRegistration{
Type: "time.timer", Type: "time.timer",
@@ -20,12 +31,12 @@ func init() {
Properties: map[string]*jsonschema.Schema{ Properties: map[string]*jsonschema.Schema{
"duration": { "duration": {
Title: "Duration", Title: "Duration",
Description: "time in milliseconds before the timer fires",
Type: "integer", Type: "integer",
Description: "Interval duration in milliseconds",
}, },
}, },
Required: []string{"duration"}, Required: []string{"duration"},
AdditionalProperties: &jsonschema.Schema{Not: &jsonschema.Schema{}}, AdditionalProperties: nil,
}, },
New: func(config config.ModuleConfig) (common.Module, error) { New: func(config config.ModuleConfig) (common.Module, error) {
params := config.Params params := config.Params
@@ -40,16 +51,6 @@ func init() {
}) })
} }
type TimeTimer struct {
config config.ModuleConfig
Duration uint32
ctx context.Context
inputHandler common.InputHandler
timer *time.Timer
logger *slog.Logger
cancel context.CancelFunc
}
func (t *TimeTimer) Id() string { func (t *TimeTimer) Id() string {
return t.config.Id return t.config.Id
} }
@@ -58,34 +59,34 @@ func (t *TimeTimer) Type() string {
return t.config.Type return t.config.Type
} }
func (t *TimeTimer) Start(ctx context.Context, inputHandler common.InputHandler) error { func (t *TimeTimer) Start(ctx context.Context) error {
t.logger.Debug("running") t.logger.Debug("running")
t.inputHandler = inputHandler router, ok := ctx.Value(common.RouterContextKey).(common.RouteIO)
if !ok {
return errors.New("time.timer unable to get router from context")
}
t.router = router
moduleContext, cancel := context.WithCancel(ctx) moduleContext, cancel := context.WithCancel(ctx)
t.ctx = moduleContext t.ctx = moduleContext
t.cancel = cancel t.cancel = cancel
t.timer = time.NewTimer(time.Millisecond * time.Duration(t.Duration)) t.timer = time.NewTimer(time.Millisecond * time.Duration(t.Duration))
for t.ctx.Err() == nil { defer t.timer.Stop()
for {
select { select {
case <-t.ctx.Done(): case <-t.ctx.Done():
t.timer.Stop()
t.logger.Debug("done")
return nil return nil
case time := <-t.timer.C: case time := <-t.timer.C:
if t.inputHandler != nil { if t.router != nil {
t.inputHandler(t.ctx, t.Id(), time) t.router.HandleInput(t.ctx, t.Id(), time)
} }
} }
} }
<-t.ctx.Done()
t.logger.Debug("done")
return nil
} }
func (t *TimeTimer) Stop() { func (t *TimeTimer) Stop() {
if t.cancel != nil { t.cancel()
defer t.cancel()
}
if t.timer != nil {
t.timer.Stop()
}
} }
+32 -36
View File
@@ -1,3 +1,5 @@
//go:build !js
package module package module
import ( import (
@@ -6,13 +8,23 @@ import (
"fmt" "fmt"
"log/slog" "log/slog"
"net" "net"
"sync"
"github.com/google/jsonschema-go/jsonschema" "github.com/google/jsonschema-go/jsonschema"
"github.com/jwetzell/showbridge-go/internal/common" "github.com/jwetzell/showbridge-go/internal/common"
"github.com/jwetzell/showbridge-go/internal/config" "github.com/jwetzell/showbridge-go/internal/config"
) )
type UDPClient struct {
config config.ModuleConfig
Addr *net.UDPAddr
Port uint16
conn *net.UDPConn
ctx context.Context
router common.RouteIO
logger *slog.Logger
cancel context.CancelFunc
}
func init() { func init() {
RegisterModule(ModuleRegistration{ RegisterModule(ModuleRegistration{
Type: "net.udp.client", Type: "net.udp.client",
@@ -21,20 +33,18 @@ func init() {
Type: "object", Type: "object",
Properties: map[string]*jsonschema.Schema{ Properties: map[string]*jsonschema.Schema{
"host": { "host": {
Title: "Host", Title: "Host",
Description: "the hostname or IP address of the UDP server to connect to", Type: "string",
Type: "string",
}, },
"port": { "port": {
Title: "Port", Title: "Port",
Description: "the port of the UDP server to connect to", Type: "integer",
Type: "integer", Minimum: jsonschema.Ptr[float64](1),
Minimum: jsonschema.Ptr[float64](1), Maximum: jsonschema.Ptr[float64](65535),
Maximum: jsonschema.Ptr[float64](65535),
}, },
}, },
Required: []string{"host", "port"}, Required: []string{"host", "port"},
AdditionalProperties: &jsonschema.Schema{Not: &jsonschema.Schema{}}, AdditionalProperties: nil,
}, },
New: func(config config.ModuleConfig) (common.Module, error) { New: func(config config.ModuleConfig) (common.Module, error) {
params := config.Params params := config.Params
@@ -57,18 +67,6 @@ func init() {
}) })
} }
type UDPClient struct {
config config.ModuleConfig
Addr *net.UDPAddr
Port uint16
conn *net.UDPConn
ctx context.Context
inputHandler common.InputHandler
logger *slog.Logger
cancel context.CancelFunc
connMu sync.Mutex
}
func (uc *UDPClient) Id() string { func (uc *UDPClient) Id() string {
return uc.config.Id return uc.config.Id
} }
@@ -78,16 +76,19 @@ func (uc *UDPClient) Type() string {
} }
func (uc *UDPClient) SetupConn() error { func (uc *UDPClient) SetupConn() error {
uc.connMu.Lock()
defer uc.connMu.Unlock()
client, err := net.DialUDP("udp", nil, uc.Addr) client, err := net.DialUDP("udp", nil, uc.Addr)
uc.conn = client uc.conn = client
return err return err
} }
func (uc *UDPClient) Start(ctx context.Context, inputHandler common.InputHandler) error { func (uc *UDPClient) Start(ctx context.Context) error {
uc.logger.Debug("running") uc.logger.Debug("running")
uc.inputHandler = inputHandler router, ok := ctx.Value(common.RouterContextKey).(common.RouteIO)
if !ok {
return errors.New("net.udp.client unable to get router from context")
}
uc.router = router
moduleContext, cancel := context.WithCancel(ctx) moduleContext, cancel := context.WithCancel(ctx)
uc.ctx = moduleContext uc.ctx = moduleContext
uc.cancel = cancel uc.cancel = cancel
@@ -99,12 +100,14 @@ func (uc *UDPClient) Start(ctx context.Context, inputHandler common.InputHandler
<-uc.ctx.Done() <-uc.ctx.Done()
uc.logger.Debug("done") uc.logger.Debug("done")
if uc.conn != nil {
uc.conn.Close()
}
return nil return nil
} }
func (uc *UDPClient) Output(ctx context.Context, payload any) error { func (uc *UDPClient) Output(ctx context.Context, payload any) error {
uc.connMu.Lock()
defer uc.connMu.Unlock()
payloadBytes, ok := common.GetAnyAsByteSlice(payload) payloadBytes, ok := common.GetAnyAsByteSlice(payload)
if !ok { if !ok {
return errors.New("net.udp.client is only able to output bytes") return errors.New("net.udp.client is only able to output bytes")
@@ -122,12 +125,5 @@ func (uc *UDPClient) Output(ctx context.Context, payload any) error {
} }
func (uc *UDPClient) Stop() { func (uc *UDPClient) Stop() {
if uc.cancel != nil { uc.cancel()
defer uc.cancel()
}
uc.connMu.Lock()
defer uc.connMu.Unlock()
if uc.conn != nil {
uc.conn.Close()
}
} }
+49 -55
View File
@@ -1,3 +1,5 @@
//go:build !js
package module package module
import ( import (
@@ -6,7 +8,6 @@ import (
"fmt" "fmt"
"log/slog" "log/slog"
"net" "net"
"sync"
"time" "time"
"github.com/google/jsonschema-go/jsonschema" "github.com/google/jsonschema-go/jsonschema"
@@ -14,6 +15,16 @@ import (
"github.com/jwetzell/showbridge-go/internal/config" "github.com/jwetzell/showbridge-go/internal/config"
) )
type UDPMulticast struct {
config config.ModuleConfig
conn *net.UDPConn
ctx context.Context
router common.RouteIO
Addr *net.UDPAddr
logger *slog.Logger
cancel context.CancelFunc
}
func init() { func init() {
RegisterModule(ModuleRegistration{ RegisterModule(ModuleRegistration{
Type: "net.udp.multicast", Type: "net.udp.multicast",
@@ -22,20 +33,18 @@ func init() {
Type: "object", Type: "object",
Properties: map[string]*jsonschema.Schema{ Properties: map[string]*jsonschema.Schema{
"ip": { "ip": {
Title: "IP", Title: "IP",
Description: "the multicast address to listen on", Type: "string",
Type: "string",
}, },
"port": { "port": {
Title: "Port", Title: "Port",
Description: "the port to listen on", Type: "integer",
Type: "integer", Minimum: jsonschema.Ptr[float64](1024),
Minimum: jsonschema.Ptr[float64](1024), Maximum: jsonschema.Ptr[float64](65535),
Maximum: jsonschema.Ptr[float64](65535),
}, },
}, },
Required: []string{"ip", "port"}, Required: []string{"ip", "port"},
AdditionalProperties: &jsonschema.Schema{Not: &jsonschema.Schema{}}, AdditionalProperties: nil,
}, },
New: func(moduleConfig config.ModuleConfig) (common.Module, error) { New: func(moduleConfig config.ModuleConfig) (common.Module, error) {
params := moduleConfig.Params params := moduleConfig.Params
@@ -58,17 +67,6 @@ func init() {
}) })
} }
type UDPMulticast struct {
config config.ModuleConfig
conn *net.UDPConn
ctx context.Context
inputHandler common.InputHandler
Addr *net.UDPAddr
logger *slog.Logger
cancel context.CancelFunc
connMu sync.Mutex
}
func (um *UDPMulticast) Id() string { func (um *UDPMulticast) Id() string {
return um.config.Id return um.config.Id
} }
@@ -77,9 +75,14 @@ func (um *UDPMulticast) Type() string {
return um.config.Type return um.config.Type
} }
func (um *UDPMulticast) Start(ctx context.Context, inputHandler common.InputHandler) error { func (um *UDPMulticast) Start(ctx context.Context) error {
um.logger.Debug("running") um.logger.Debug("running")
um.inputHandler = inputHandler router, ok := ctx.Value(common.RouterContextKey).(common.RouteIO)
if !ok {
return errors.New("net.udp.multicast unable to get router from context")
}
um.router = router
moduleContext, cancel := context.WithCancel(ctx) moduleContext, cancel := context.WithCancel(ctx)
um.ctx = moduleContext um.ctx = moduleContext
um.cancel = cancel um.cancel = cancel
@@ -90,40 +93,38 @@ func (um *UDPMulticast) Start(ctx context.Context, inputHandler common.InputHand
} }
defer client.Close() defer client.Close()
um.connMu.Lock()
um.conn = client um.conn = client
um.connMu.Unlock()
buffer := make([]byte, 2048) buffer := make([]byte, 2048)
for um.ctx.Err() == nil { for {
um.conn.SetDeadline(time.Now().Add(time.Millisecond * 200)) select {
case <-um.ctx.Done():
// TODO(jwetzell): cleanup?
um.logger.Debug("done")
return nil
default:
um.conn.SetDeadline(time.Now().Add(time.Millisecond * 200))
numBytes, _, err := um.conn.ReadFromUDP(buffer) numBytes, _, err := um.conn.ReadFromUDP(buffer)
if err != nil { if err != nil {
if errors.Is(err, net.ErrClosed) { //NOTE(jwetzell) we hit deadline
break if opErr, ok := err.(*net.OpError); ok && opErr.Timeout() {
continue
}
return err
} }
//NOTE(jwetzell) we hit deadline
opErr, ok := err.(*net.OpError)
if ok && opErr.Timeout() {
continue
}
return err
}
if numBytes > 0 { if numBytes > 0 {
message := buffer[:numBytes] message := buffer[:numBytes]
if um.inputHandler != nil { if um.router != nil {
um.inputHandler(um.ctx, um.Id(), message) um.router.HandleInput(um.ctx, um.Id(), message)
} else { } else {
um.logger.Error("input received but no input handler is configured") um.logger.Error("input received but no router is configured")
}
} }
} }
} }
<-um.ctx.Done()
um.logger.Debug("done")
return nil
} }
func (um *UDPMulticast) Output(ctx context.Context, payload any) error { func (um *UDPMulticast) Output(ctx context.Context, payload any) error {
@@ -142,12 +143,5 @@ func (um *UDPMulticast) Output(ctx context.Context, payload any) error {
} }
func (um *UDPMulticast) Stop() { func (um *UDPMulticast) Stop() {
if um.cancel != nil { um.cancel()
defer um.cancel()
}
um.connMu.Lock()
defer um.connMu.Unlock()
if um.conn != nil {
um.conn.Close()
}
} }
+58 -60
View File
@@ -1,3 +1,5 @@
//go:build !js
package module package module
import ( import (
@@ -7,7 +9,6 @@ import (
"fmt" "fmt"
"log/slog" "log/slog"
"net" "net"
"sync"
"time" "time"
"github.com/google/jsonschema-go/jsonschema" "github.com/google/jsonschema-go/jsonschema"
@@ -15,6 +16,16 @@ import (
"github.com/jwetzell/showbridge-go/internal/config" "github.com/jwetzell/showbridge-go/internal/config"
) )
type UDPServer struct {
Addr *net.UDPAddr
BufferSize int
config config.ModuleConfig
ctx context.Context
router common.RouteIO
logger *slog.Logger
cancel context.CancelFunc
}
func init() { func init() {
RegisterModule(ModuleRegistration{ RegisterModule(ModuleRegistration{
Type: "net.udp.server", Type: "net.udp.server",
@@ -23,29 +34,26 @@ func init() {
Type: "object", Type: "object",
Properties: map[string]*jsonschema.Schema{ Properties: map[string]*jsonschema.Schema{
"ip": { "ip": {
Title: "IP", Title: "IP",
Description: "the IP address to bind the UDP server to", Type: "string",
Type: "string", Default: json.RawMessage(`"0.0.0.0"`),
Default: json.RawMessage(`"0.0.0.0"`),
}, },
"port": { "port": {
Title: "Port", Title: "Port",
Description: "the port for the UDP server to listen on", Type: "integer",
Type: "integer", Minimum: jsonschema.Ptr[float64](1024),
Minimum: jsonschema.Ptr[float64](1024), Maximum: jsonschema.Ptr[float64](65535),
Maximum: jsonschema.Ptr[float64](65535),
}, },
"bufferSize": { "bufferSize": {
Title: "Buffer Size", Title: "Buffer Size",
Description: "the size of the buffer for incoming UDP messages", Type: "integer",
Type: "integer", Minimum: jsonschema.Ptr[float64](1),
Minimum: jsonschema.Ptr[float64](1), Maximum: jsonschema.Ptr[float64](65535),
Maximum: jsonschema.Ptr[float64](65535), Default: json.RawMessage("2048"),
Default: json.RawMessage("2048"),
}, },
}, },
Required: []string{"port"}, Required: []string{"port"},
AdditionalProperties: &jsonschema.Schema{Not: &jsonschema.Schema{}}, AdditionalProperties: nil,
}, },
New: func(moduleConfig config.ModuleConfig) (common.Module, error) { New: func(moduleConfig config.ModuleConfig) (common.Module, error) {
params := moduleConfig.Params params := moduleConfig.Params
@@ -81,18 +89,6 @@ func init() {
}) })
} }
type UDPServer struct {
Addr *net.UDPAddr
BufferSize int
config config.ModuleConfig
ctx context.Context
inputHandler common.InputHandler
logger *slog.Logger
cancel context.CancelFunc
listener *net.UDPConn
listenerMu sync.Mutex
}
func (us *UDPServer) Id() string { func (us *UDPServer) Id() string {
return us.config.Id return us.config.Id
} }
@@ -101,9 +97,14 @@ func (us *UDPServer) Type() string {
return us.config.Type return us.config.Type
} }
func (us *UDPServer) Start(ctx context.Context, inputHandler common.InputHandler) error { func (us *UDPServer) Start(ctx context.Context) error {
us.logger.Debug("running") us.logger.Debug("running")
us.inputHandler = inputHandler router, ok := ctx.Value(common.RouterContextKey).(common.RouteIO)
if !ok {
return errors.New("net.udp.server unable to get router from context")
}
us.router = router
moduleContext, cancel := context.WithCancel(ctx) moduleContext, cancel := context.WithCancel(ctx)
us.ctx = moduleContext us.ctx = moduleContext
us.cancel = cancel us.cancel = cancel
@@ -112,33 +113,37 @@ func (us *UDPServer) Start(ctx context.Context, inputHandler common.InputHandler
if err != nil { if err != nil {
return err return err
} }
us.listenerMu.Lock()
us.listener = listener defer listener.Close()
us.listenerMu.Unlock()
buffer := make([]byte, us.BufferSize) buffer := make([]byte, us.BufferSize)
for us.ctx.Err() == nil { for {
listener.SetDeadline(time.Now().Add(time.Millisecond * 200)) select {
case <-us.ctx.Done():
// TODO(jwetzell): cleanup?
us.logger.Debug("done")
return nil
default:
listener.SetDeadline(time.Now().Add(time.Millisecond * 200))
numBytes, _, err := listener.ReadFromUDP(buffer) numBytes, senderAddr, err := listener.ReadFromUDP(buffer)
if err != nil { if err != nil {
//NOTE(jwetzell) we hit deadline //NOTE(jwetzell) we hit deadline
opErr, ok := err.(*net.OpError) if opErr, ok := err.(*net.OpError); ok && opErr.Timeout() {
if ok && opErr.Timeout() { continue
continue }
return err
}
message := buffer[:numBytes]
if us.router != nil {
senderCtx := context.WithValue(us.ctx, common.SenderContextKey, senderAddr)
us.router.HandleInput(senderCtx, us.Id(), message)
} else {
us.logger.Error("input received but no router is configured")
} }
break
}
message := buffer[:numBytes]
if us.inputHandler != nil {
us.inputHandler(us.ctx, us.Id(), message)
} else {
us.logger.Error("input received but no input handler is configured")
} }
} }
<-us.ctx.Done()
us.logger.Debug("done")
return nil
} }
func (us *UDPServer) Output(ctx context.Context, payload any) error { func (us *UDPServer) Output(ctx context.Context, payload any) error {
@@ -146,12 +151,5 @@ func (us *UDPServer) Output(ctx context.Context, payload any) error {
} }
func (us *UDPServer) Stop() { func (us *UDPServer) Stop() {
if us.cancel != nil { us.cancel()
defer us.cancel()
}
us.listenerMu.Lock()
defer us.listenerMu.Unlock()
if us.listener != nil {
us.listener.Close()
}
} }
+91
View File
@@ -0,0 +1,91 @@
//go:build js
package module
import (
"context"
"errors"
"fmt"
"log/slog"
"syscall/js"
"time"
"github.com/google/jsonschema-go/jsonschema"
"github.com/jwetzell/showbridge-go/internal/common"
"github.com/jwetzell/showbridge-go/internal/config"
)
type WebOnClick struct {
config config.ModuleConfig
ctx context.Context
router common.RouteIO
logger *slog.Logger
ElementId string
}
func init() {
RegisterModule(ModuleRegistration{
Type: "web.onclick",
Title: "On Click",
ParamsSchema: &jsonschema.Schema{
Type: "object",
Properties: map[string]*jsonschema.Schema{
"id": {
Title: "Element ID",
Type: "string",
Description: "ID of the HTML element to attach the click listener to",
},
},
Required: []string{"duration"},
AdditionalProperties: nil,
},
New: func(config config.ModuleConfig) (common.Module, error) {
params := config.Params
idString, err := params.GetString("id")
if err != nil {
return nil, fmt.Errorf("web.onclick id error: %w", err)
}
return &WebOnClick{ElementId: idString, config: config, logger: CreateLogger(config)}, nil
},
})
}
func (woc *WebOnClick) Id() string {
return woc.config.Id
}
func (woc *WebOnClick) Type() string {
return woc.config.Type
}
func (woc *WebOnClick) Start(ctx context.Context) error {
woc.logger.Debug("running")
router, ok := ctx.Value(common.RouterContextKey).(common.RouteIO)
if !ok {
return errors.New("net.tcp.client unable to get router from context")
}
woc.router = router
woc.ctx = ctx
element := js.Global().Get("document").Call("getElementById", woc.ElementId)
if element.IsNull() || element.IsUndefined() {
return fmt.Errorf("web.onclick unable to find element with id: %s", woc.ElementId)
}
element.Set("onclick", js.FuncOf(func(js.Value, []js.Value) interface{} {
if woc.router != nil {
woc.router.HandleInput(woc.ctx, woc.Id(), time.Now())
}
return nil
}))
<-ctx.Done()
return nil
}
func (woc *WebOnClick) Stop() {
}
+7
View File
@@ -0,0 +1,7 @@
//go:build js
package module
import (
_ "gitlab.com/gomidi/midi/v2/drivers/webmididrv"
)
-217
View File
@@ -1,217 +0,0 @@
package module
import (
"context"
"errors"
"fmt"
"log/slog"
"net/url"
"sync"
"time"
"github.com/google/jsonschema-go/jsonschema"
"github.com/gorilla/websocket"
"github.com/jwetzell/showbridge-go/internal/common"
"github.com/jwetzell/showbridge-go/internal/config"
)
func init() {
RegisterModule(ModuleRegistration{
Type: "websocket.client",
Title: "WebSocket Client",
ParamsSchema: &jsonschema.Schema{
Type: "object",
Properties: map[string]*jsonschema.Schema{
"url": {
Title: "URL",
Description: "the URL of the WebSocket server to connect",
Type: "string",
},
},
Required: []string{"url"},
AdditionalProperties: &jsonschema.Schema{Not: &jsonschema.Schema{}},
},
New: func(config config.ModuleConfig) (common.Module, error) {
params := config.Params
urlString, err := params.GetString("url")
if err != nil {
return nil, fmt.Errorf("websocket.client url error: %w", err)
}
parsedURL, err := url.Parse(urlString)
if err != nil {
return nil, fmt.Errorf("websocket.client url error: %w", err)
}
if parsedURL.Scheme != "ws" && parsedURL.Scheme != "wss" {
return nil, fmt.Errorf("websocket.client url error: scheme must be ws or wss")
}
return &WebSocketClient{URL: *parsedURL, config: config, logger: CreateLogger(config)}, nil
},
})
}
type WebSocketClient struct {
config config.ModuleConfig
URL url.URL
ctx context.Context
conn *websocket.Conn
inputHandler common.InputHandler
logger *slog.Logger
cancel context.CancelFunc
connMu sync.Mutex
}
func (wc *WebSocketClient) Id() string {
return wc.config.Id
}
func (wc *WebSocketClient) Type() string {
return wc.config.Type
}
func (wc *WebSocketClient) SetupConn() error {
wc.connMu.Lock()
defer wc.connMu.Unlock()
if wc.conn != nil {
wc.conn.Close()
}
conn, _, err := websocket.DefaultDialer.Dial(wc.URL.String(), nil)
if err != nil {
return fmt.Errorf("websocket.client dial error: %w", err)
}
conn.SetCloseHandler(func(code int, text string) error {
// NOTE(jwetzell): attempt to send close message back to server before closing connection
err := wc.conn.WriteControl(
websocket.CloseMessage,
websocket.FormatCloseMessage(websocket.CloseNormalClosure, ""),
time.Now().Add(time.Minute),
)
wc.connMu.Lock()
defer wc.connMu.Unlock()
if wc.conn != nil {
wc.conn.Close()
}
return err
})
wc.conn = conn
return err
}
func (wc *WebSocketClient) Start(ctx context.Context, inputHandler common.InputHandler) error {
wc.logger.Debug("running")
wc.inputHandler = inputHandler
moduleContext, cancel := context.WithCancel(ctx)
wc.ctx = moduleContext
wc.cancel = cancel
for wc.ctx.Err() == nil {
err := wc.SetupConn()
if err != nil {
wc.logger.Error("connection error", "error", err)
} else {
// NOTE(jwetzell): enter read loop until an error occurs
wc.readLoop()
}
// NOTE(jwetzell): if connection is lost or read error wait before trying again
time.Sleep(2 * time.Second)
}
<-wc.ctx.Done()
wc.logger.Debug("done")
return nil
}
func (wc *WebSocketClient) readLoop() {
for wc.ctx.Err() == nil {
if wc.conn == nil {
wc.logger.Error("websocket connection is not established")
return
}
// TODO(jwetzell): other ways to timeout?
wc.conn.SetReadDeadline(time.Now().Add(5 * time.Second))
messageType, message, err := wc.conn.ReadMessage()
if err != nil {
return
}
if wc.inputHandler != nil {
switch messageType {
case websocket.CloseMessage:
return
case websocket.PingMessage:
err := wc.conn.WriteMessage(websocket.PongMessage, nil)
if err != nil {
wc.logger.Error("websocket pong error", "error", err)
return
}
case websocket.TextMessage:
wc.inputHandler(wc.ctx, wc.Id(), string(message))
case websocket.BinaryMessage:
wc.inputHandler(wc.ctx, wc.Id(), message)
default:
wc.logger.Warn("unsupported message type received", "messageType", messageType)
}
} else {
wc.logger.Error("input received but no input handler is configured")
continue
}
}
}
func (wc *WebSocketClient) outputBytes(ctx context.Context, payload []byte) error {
if wc.conn == nil {
return errors.New("websocket.client client is not setup")
}
err := wc.conn.WriteMessage(websocket.BinaryMessage, payload)
if err != nil {
return err
}
return nil
}
func (wc *WebSocketClient) outputString(ctx context.Context, payload string) error {
if wc.conn == nil {
return errors.New("websocket.client client is not setup")
}
err := wc.conn.WriteMessage(websocket.TextMessage, []byte(payload))
if err != nil {
return err
}
return nil
}
func (wc *WebSocketClient) Output(ctx context.Context, payload any) error {
wc.connMu.Lock()
defer wc.connMu.Unlock()
payloadBytes, ok := common.GetAnyAsByteSlice(payload)
if ok {
return wc.outputBytes(ctx, payloadBytes)
} else {
payloadString, ok := common.GetAnyAs[string](payload)
if ok {
return wc.outputString(ctx, payloadString)
} else {
return errors.New("websocket.client payload must be string or []byte")
}
}
}
func (wc *WebSocketClient) Stop() {
if wc.cancel != nil {
defer wc.cancel()
}
wc.connMu.Lock()
defer wc.connMu.Unlock()
if wc.conn != nil {
err := wc.conn.WriteControl(websocket.CloseMessage, nil, time.Now().Add(time.Minute))
if err != nil {
wc.logger.Error("websocket close error", "error", err)
}
wc.conn.Close()
}
}
+13 -11
View File
@@ -1,3 +1,5 @@
//go:build !js
package processor package processor
import ( import (
@@ -9,16 +11,6 @@ import (
"github.com/jwetzell/showbridge-go/internal/config" "github.com/jwetzell/showbridge-go/internal/config"
) )
func init() {
RegisterProcessor(ProcessorRegistration{
Type: "artnet.packet.decode",
Title: "Decode ArtNet Packet",
New: func(config config.ProcessorConfig) (Processor, error) {
return &ArtNetPacketDecode{config: config}, nil
},
})
}
type ArtNetPacketDecode struct { type ArtNetPacketDecode struct {
config config.ProcessorConfig config config.ProcessorConfig
} }
@@ -46,4 +38,14 @@ func (apd *ArtNetPacketDecode) Process(ctx context.Context, wrappedPayload commo
func (apd *ArtNetPacketDecode) Type() string { func (apd *ArtNetPacketDecode) Type() string {
return apd.config.Type return apd.config.Type
} }
func init() {
RegisterProcessor(ProcessorRegistration{
Type: "artnet.packet.decode",
Title: "Decode ArtNet Packet",
New: func(config config.ProcessorConfig) (Processor, error) {
return &ArtNetPacketDecode{config: config}, nil
},
})
}
+12 -10
View File
@@ -1,3 +1,5 @@
//go:build !js
package processor package processor
import ( import (
@@ -9,16 +11,6 @@ import (
"github.com/jwetzell/showbridge-go/internal/config" "github.com/jwetzell/showbridge-go/internal/config"
) )
func init() {
RegisterProcessor(ProcessorRegistration{
Type: "artnet.packet.encode",
Title: "Encode ArtNet Packet",
New: func(config config.ProcessorConfig) (Processor, error) {
return &ArtNetPacketEncode{config: config}, nil
},
})
}
type ArtNetPacketEncode struct { type ArtNetPacketEncode struct {
config config.ProcessorConfig config config.ProcessorConfig
} }
@@ -46,3 +38,13 @@ func (ape *ArtNetPacketEncode) Process(ctx context.Context, wrappedPayload commo
func (ape *ArtNetPacketEncode) Type() string { func (ape *ArtNetPacketEncode) Type() string {
return ape.config.Type return ape.config.Type
} }
func init() {
RegisterProcessor(ProcessorRegistration{
Type: "artnet.packet.encode",
Title: "Encode ArtNet Packet",
New: func(config config.ProcessorConfig) (Processor, error) {
return &ArtNetPacketEncode{config: config}, nil
},
})
}
+90 -107
View File
@@ -8,32 +8,104 @@ import (
"log/slog" "log/slog"
"text/template" "text/template"
"github.com/google/jsonschema-go/jsonschema"
"github.com/jwetzell/showbridge-go/internal/common" "github.com/jwetzell/showbridge-go/internal/common"
"github.com/jwetzell/showbridge-go/internal/config" "github.com/jwetzell/showbridge-go/internal/config"
) )
type DbQuery struct {
config config.ProcessorConfig
ModuleId string
Query *template.Template
logger *slog.Logger
}
func (dq *DbQuery) Process(ctx context.Context, wrappedPayload common.WrappedPayload) (common.WrappedPayload, error) {
if wrappedPayload.Modules == nil {
wrappedPayload.End = true
return wrappedPayload, errors.New("db.query wrapped payload has no modules")
}
module, ok := wrappedPayload.Modules[dq.ModuleId]
if !ok {
wrappedPayload.End = true
return wrappedPayload, fmt.Errorf("db.query unable to find module with id: %s", dq.ModuleId)
}
dbModule, ok := module.(common.DatabaseModule)
if !ok {
wrappedPayload.End = true
return wrappedPayload, fmt.Errorf("db.query module with id %s is not a DatabaseModule", dq.ModuleId)
}
db := dbModule.Database()
if db == nil {
wrappedPayload.End = true
return wrappedPayload, fmt.Errorf("db.query module with id %s returned nil database", dq.ModuleId)
}
var queryBuffer bytes.Buffer
err := dq.Query.Execute(&queryBuffer, wrappedPayload)
if err != nil {
wrappedPayload.End = true
return wrappedPayload, err
}
// support proper parameterized queries
rows, err := db.QueryContext(ctx, queryBuffer.String())
if err != nil {
wrappedPayload.End = true
return wrappedPayload, fmt.Errorf("db.query error executing query: %w", err)
}
defer rows.Close()
columns, err := rows.Columns()
if err != nil {
wrappedPayload.End = true
return wrappedPayload, fmt.Errorf("db.query error getting columns: %w", err)
}
results := make([]map[string]any, 0)
for rows.Next() {
columnValues := make([]interface{}, len(columns))
for i := range columnValues {
columnValues[i] = new(interface{})
}
if err := rows.Scan(columnValues...); err != nil {
wrappedPayload.End = true
return wrappedPayload, fmt.Errorf("db.query error scanning row: %w", err)
}
rowMap := make(map[string]any)
for i, colName := range columns {
value := *columnValues[i].(*interface{})
rowMap[colName] = value
}
results = append(results, rowMap)
}
if len(results) == 0 {
wrappedPayload.Payload = nil
return wrappedPayload, nil
} else if len(results) == 1 {
wrappedPayload.Payload = results[0]
return wrappedPayload, nil
}
wrappedPayload.Payload = results
return wrappedPayload, nil
}
func (dq *DbQuery) Type() string {
return dq.config.Type
}
func init() { func init() {
RegisterProcessor(ProcessorRegistration{ RegisterProcessor(ProcessorRegistration{
Type: "db.query", Type: "db.query",
Title: "Query Database", Title: "Query Database",
ParamsSchema: &jsonschema.Schema{
Type: "object",
Properties: map[string]*jsonschema.Schema{
"module": {
Title: "Module ID",
Description: "ID of the database module to query",
Type: "string",
},
"query": {
Title: "Query",
Description: "SQL query to execute",
Type: "string",
},
},
Required: []string{"module", "query"},
AdditionalProperties: &jsonschema.Schema{Not: &jsonschema.Schema{}},
},
New: func(config config.ProcessorConfig) (Processor, error) { New: func(config config.ProcessorConfig) (Processor, error) {
params := config.Params params := config.Params
@@ -57,92 +129,3 @@ func init() {
}, },
}) })
} }
type DbQuery struct {
config config.ProcessorConfig
ModuleId string
Query *template.Template
logger *slog.Logger
module common.DatabaseModule
}
func (dq *DbQuery) Process(ctx context.Context, wrappedPayload common.WrappedPayload) (common.WrappedPayload, error) {
if dq.module == nil {
if wrappedPayload.Modules == nil {
wrappedPayload.End = true
return wrappedPayload, errors.New("db.query wrapped payload has no modules")
}
module, ok := wrappedPayload.Modules[dq.ModuleId]
if !ok {
wrappedPayload.End = true
return wrappedPayload, fmt.Errorf("db.query unable to find module with id: %s", dq.ModuleId)
}
dbModule, ok := module.(common.DatabaseModule)
if !ok {
wrappedPayload.End = true
return wrappedPayload, fmt.Errorf("db.query module with id %s is not a DatabaseModule", dq.ModuleId)
}
dq.module = dbModule
}
var queryBuffer bytes.Buffer
err := dq.Query.Execute(&queryBuffer, wrappedPayload)
if err != nil {
wrappedPayload.End = true
return wrappedPayload, err
}
// support proper parameterized queries
rows, err := dq.module.QueryContext(ctx, queryBuffer.String())
if err != nil {
wrappedPayload.End = true
return wrappedPayload, fmt.Errorf("db.query error executing query: %w", err)
}
defer rows.Close()
columns, err := rows.Columns()
if err != nil {
wrappedPayload.End = true
return wrappedPayload, fmt.Errorf("db.query error getting columns: %w", err)
}
// TODO(jwetzell): optimize this
results := make([]map[string]any, 0)
for rows.Next() {
columnValues := make([]any, len(columns))
for i := range columnValues {
columnValues[i] = new(any)
}
if err := rows.Scan(columnValues...); err != nil {
wrappedPayload.End = true
return wrappedPayload, fmt.Errorf("db.query error scanning row: %w", err)
}
rowMap := make(map[string]any)
for i, colName := range columns {
value := *columnValues[i].(*any)
rowMap[colName] = value
}
results = append(results, rowMap)
}
if len(results) == 0 {
wrappedPayload.Payload = nil
return wrappedPayload, nil
} else if len(results) == 1 {
wrappedPayload.Payload = results[0]
return wrappedPayload, nil
}
wrappedPayload.Payload = results
return wrappedPayload, nil
}
func (dq *DbQuery) Type() string {
return dq.config.Type
}
+9 -11
View File
@@ -9,22 +9,11 @@ import (
"github.com/jwetzell/showbridge-go/internal/config" "github.com/jwetzell/showbridge-go/internal/config"
) )
func init() {
RegisterProcessor(ProcessorRegistration{
Type: "debug.log",
Title: "Debug Log",
New: func(config config.ProcessorConfig) (Processor, error) {
return &DebugLog{config: config, logger: slog.Default().With("component", "processor", "type", config.Type)}, nil
},
})
}
type DebugLog struct { type DebugLog struct {
config config.ProcessorConfig config config.ProcessorConfig
logger *slog.Logger logger *slog.Logger
} }
// TODO(jwetzell): maybe make a more useful logging processor
func (dl *DebugLog) Process(ctx context.Context, wrappedPayload common.WrappedPayload) (common.WrappedPayload, error) { func (dl *DebugLog) Process(ctx context.Context, wrappedPayload common.WrappedPayload) (common.WrappedPayload, error) {
payload := wrappedPayload.Payload payload := wrappedPayload.Payload
payloadString := fmt.Sprintf("%+v", payload) payloadString := fmt.Sprintf("%+v", payload)
@@ -37,3 +26,12 @@ func (dl *DebugLog) Type() string {
return dl.config.Type return dl.config.Type
} }
func init() {
RegisterProcessor(ProcessorRegistration{
Type: "debug.log",
Title: "Debug Log",
New: func(config config.ProcessorConfig) (Processor, error) {
return &DebugLog{config: config, logger: slog.Default().With("component", "processor", "type", config.Type)}, nil
},
})
}
+10 -10
View File
@@ -8,16 +8,6 @@ import (
"github.com/jwetzell/showbridge-go/internal/config" "github.com/jwetzell/showbridge-go/internal/config"
) )
func init() {
RegisterProcessor(ProcessorRegistration{
Type: "filter.change",
Title: "Filter On Change",
New: func(config config.ProcessorConfig) (Processor, error) {
return &FilterChange{config: config}, nil
},
})
}
type FilterChange struct { type FilterChange struct {
config config.ProcessorConfig config config.ProcessorConfig
previous any previous any
@@ -38,3 +28,13 @@ func (fc *FilterChange) Process(ctx context.Context, wrappedPayload common.Wrapp
func (fc *FilterChange) Type() string { func (fc *FilterChange) Type() string {
return fc.config.Type return fc.config.Type
} }
func init() {
RegisterProcessor(ProcessorRegistration{
Type: "filter.change",
Title: "Filter On Change",
New: func(config config.ProcessorConfig) (Processor, error) {
return &FilterChange{config: config}, nil
},
})
}
+33 -34
View File
@@ -11,40 +11,6 @@ import (
"github.com/jwetzell/showbridge-go/internal/config" "github.com/jwetzell/showbridge-go/internal/config"
) )
func init() {
RegisterProcessor(ProcessorRegistration{
Type: "filter.expr",
Title: "Filter by Expr expression",
ParamsSchema: &jsonschema.Schema{
Type: "object",
Properties: map[string]*jsonschema.Schema{
"expression": {
Title: "Expression",
Description: "Expr expression to evaluate, must return a boolean",
Type: "string",
},
},
Required: []string{"expression"},
AdditionalProperties: &jsonschema.Schema{Not: &jsonschema.Schema{}},
},
New: func(config config.ProcessorConfig) (Processor, error) {
params := config.Params
expressionString, err := params.GetString("expression")
if err != nil {
return nil, fmt.Errorf("filter.expr expression error: %w", err)
}
program, err := expr.Compile(expressionString)
if err != nil {
return nil, err
}
return &FilterExpr{config: config, Program: program}, nil
},
})
}
// NOTE(jwetzell): see language definition https://expr-lang.org/docs/language-definition // NOTE(jwetzell): see language definition https://expr-lang.org/docs/language-definition
type FilterExpr struct { type FilterExpr struct {
config config.ProcessorConfig config config.ProcessorConfig
@@ -79,3 +45,36 @@ func (fe *FilterExpr) Process(ctx context.Context, wrappedPayload common.Wrapped
func (fe *FilterExpr) Type() string { func (fe *FilterExpr) Type() string {
return fe.config.Type return fe.config.Type
} }
func init() {
RegisterProcessor(ProcessorRegistration{
Type: "filter.expr",
Title: "Filter by Expr expression",
ParamsSchema: &jsonschema.Schema{
Type: "object",
Properties: map[string]*jsonschema.Schema{
"expression": {
Title: "Expression",
Type: "string",
},
},
Required: []string{"expression"},
AdditionalProperties: nil,
},
New: func(config config.ProcessorConfig) (Processor, error) {
params := config.Params
expressionString, err := params.GetString("expression")
if err != nil {
return nil, fmt.Errorf("filter.expr expression error: %w", err)
}
program, err := expr.Compile(expressionString)
if err != nil {
return nil, err
}
return &FilterExpr{config: config, Program: program}, nil
},
})
}
-58
View File
@@ -1,58 +0,0 @@
package processor
import (
"context"
"fmt"
"github.com/google/jsonschema-go/jsonschema"
"github.com/jwetzell/showbridge-go/internal/common"
"github.com/jwetzell/showbridge-go/internal/config"
"golang.org/x/time/rate"
)
func init() {
RegisterProcessor(ProcessorRegistration{
Type: "filter.rate",
Title: "Filter by Rate",
ParamsSchema: &jsonschema.Schema{
Type: "object",
Properties: map[string]*jsonschema.Schema{
"rate": {
Title: "Rate",
Description: "number of events to allow per second",
Type: "integer",
},
},
Required: []string{"rate"},
},
New: func(config config.ProcessorConfig) (Processor, error) {
params := config.Params
rateInt, err := params.GetInt("rate")
if err != nil {
return nil, fmt.Errorf("filter.rate rate error: %w", err)
}
limiter := rate.NewLimiter(rate.Limit(rateInt), rateInt*2)
return &FilterRate{config: config, limiter: limiter}, nil
},
})
}
type FilterRate struct {
config config.ProcessorConfig
limiter *rate.Limiter
}
func (fc *FilterRate) Process(ctx context.Context, wrappedPayload common.WrappedPayload) (common.WrappedPayload, error) {
err := fc.limiter.Wait(ctx)
if err != nil {
wrappedPayload.End = true
return wrappedPayload, err
}
return wrappedPayload, nil
}
func (fc *FilterRate) Type() string {
return fc.config.Type
}
+34 -35
View File
@@ -11,41 +11,6 @@ import (
"github.com/jwetzell/showbridge-go/internal/config" "github.com/jwetzell/showbridge-go/internal/config"
) )
func init() {
RegisterProcessor(ProcessorRegistration{
Type: "filter.regex",
Title: "Filter by Regex",
ParamsSchema: &jsonschema.Schema{
Type: "object",
Properties: map[string]*jsonschema.Schema{
"pattern": {
Title: "Pattern",
Description: "regex pattern to match against",
Type: "string",
},
},
Required: []string{"pattern"},
AdditionalProperties: &jsonschema.Schema{Not: &jsonschema.Schema{}},
},
New: func(config config.ProcessorConfig) (Processor, error) {
params := config.Params
patternString, err := params.GetString("pattern")
if err != nil {
return nil, fmt.Errorf("filter.regex pattern error: %w", err)
}
patternRegexp, err := regexp.Compile(patternString)
if err != nil {
return nil, err
}
return &FilterRegex{config: config, Pattern: patternRegexp}, nil
},
})
}
type FilterRegex struct { type FilterRegex struct {
config config.ProcessorConfig config config.ProcessorConfig
Pattern *regexp.Regexp Pattern *regexp.Regexp
@@ -72,3 +37,37 @@ func (fr *FilterRegex) Process(ctx context.Context, wrappedPayload common.Wrappe
func (fr *FilterRegex) Type() string { func (fr *FilterRegex) Type() string {
return fr.config.Type return fr.config.Type
} }
func init() {
RegisterProcessor(ProcessorRegistration{
Type: "filter.regex",
Title: "Filter by Regex",
ParamsSchema: &jsonschema.Schema{
Type: "object",
Properties: map[string]*jsonschema.Schema{
"pattern": {
Title: "Pattern",
Type: "string",
},
},
Required: []string{"pattern"},
AdditionalProperties: nil,
},
New: func(config config.ProcessorConfig) (Processor, error) {
params := config.Params
patternString, err := params.GetString("pattern")
if err != nil {
return nil, fmt.Errorf("filter.regex pattern error: %w", err)
}
patternRegexp, err := regexp.Compile(patternString)
if err != nil {
return nil, err
}
return &FilterRegex{config: config, Pattern: patternRegexp}, nil
},
})
}
+32 -33
View File
@@ -12,39 +12,6 @@ import (
"github.com/jwetzell/showbridge-go/internal/config" "github.com/jwetzell/showbridge-go/internal/config"
) )
func init() {
RegisterProcessor(ProcessorRegistration{
Type: "float.parse",
Title: "Parse Float",
ParamsSchema: &jsonschema.Schema{
Type: "object",
Properties: map[string]*jsonschema.Schema{
"bitSize": {
Title: "Bit Size",
Description: "bit size of the resulting float",
Type: "integer",
Enum: []any{32, 64},
Default: json.RawMessage("64"),
},
},
AdditionalProperties: &jsonschema.Schema{Not: &jsonschema.Schema{}},
},
New: func(moduleConfig config.ProcessorConfig) (Processor, error) {
params := moduleConfig.Params
bitSizeNum, err := params.GetInt("bitSize")
if err != nil {
if errors.Is(err, config.ErrParamNotFound) {
bitSizeNum = 64
} else {
return nil, fmt.Errorf("float.parse bitSize error: %w", err)
}
}
return &FloatParse{config: moduleConfig, BitSize: bitSizeNum}, nil
},
})
}
type FloatParse struct { type FloatParse struct {
BitSize int BitSize int
config config.ProcessorConfig config config.ProcessorConfig
@@ -71,3 +38,35 @@ func (fp *FloatParse) Process(ctx context.Context, wrappedPayload common.Wrapped
func (fp *FloatParse) Type() string { func (fp *FloatParse) Type() string {
return fp.config.Type return fp.config.Type
} }
func init() {
RegisterProcessor(ProcessorRegistration{
Type: "float.parse",
Title: "Parse Float",
ParamsSchema: &jsonschema.Schema{
Type: "object",
Properties: map[string]*jsonschema.Schema{
"bitSize": {
Title: "Bit Size",
Type: "integer",
Enum: []any{32, 64},
Default: json.RawMessage("64"),
},
},
AdditionalProperties: nil,
},
New: func(moduleConfig config.ProcessorConfig) (Processor, error) {
params := moduleConfig.Params
bitSizeNum, err := params.GetInt("bitSize")
if err != nil {
if errors.Is(err, config.ErrParamNotFound) {
bitSizeNum = 64
} else {
return nil, fmt.Errorf("float.parse bitSize error: %w", err)
}
}
return &FloatParse{config: moduleConfig, BitSize: bitSizeNum}, nil
},
})
}
+35 -38
View File
@@ -12,6 +12,32 @@ import (
"github.com/jwetzell/showbridge-go/internal/config" "github.com/jwetzell/showbridge-go/internal/config"
) )
type FloatRandom struct {
BitSize int
Min float64
Max float64
config config.ProcessorConfig
}
func (fr *FloatRandom) Process(ctx context.Context, wrappedPayload common.WrappedPayload) (common.WrappedPayload, error) {
if fr.BitSize == 32 {
payloadFloat := rand.Float32()*(float32(fr.Max)-float32(fr.Min)) + float32(fr.Min)
wrappedPayload.Payload = payloadFloat
return wrappedPayload, nil
}
if fr.BitSize == 64 {
payloadFloat := rand.Float64()*(fr.Max-fr.Min) + fr.Min
wrappedPayload.Payload = payloadFloat
return wrappedPayload, nil
}
wrappedPayload.End = true
return wrappedPayload, errors.New("float.random bitSize error: must be 32 or 64")
}
func (fr *FloatRandom) Type() string {
return fr.config.Type
}
func init() { func init() {
RegisterProcessor(ProcessorRegistration{ RegisterProcessor(ProcessorRegistration{
Type: "float.random", Type: "float.random",
@@ -20,25 +46,22 @@ func init() {
Type: "object", Type: "object",
Properties: map[string]*jsonschema.Schema{ Properties: map[string]*jsonschema.Schema{
"bitSize": { "bitSize": {
Title: "Bit Size", Title: "Bit Size",
Description: "bit size of the resulting float", Type: "integer",
Type: "integer", Enum: []any{32, 64},
Enum: []any{32, 64}, Default: json.RawMessage("32"),
Default: json.RawMessage("32"),
}, },
"min": { "min": {
Title: "Minimum", Title: "Minimum",
Description: "inclusive minimum value of the random float", Type: "number",
Type: "number",
}, },
"max": { "max": {
Title: "Maximum", Title: "Maximum",
Description: "exclusive maximum value of the random float", Type: "number",
Type: "number",
}, },
}, },
Required: []string{"min", "max"}, Required: []string{"min", "max"},
AdditionalProperties: &jsonschema.Schema{Not: &jsonschema.Schema{}}, AdditionalProperties: nil,
}, },
New: func(processorConfig config.ProcessorConfig) (Processor, error) { New: func(processorConfig config.ProcessorConfig) (Processor, error) {
params := processorConfig.Params params := processorConfig.Params
@@ -74,29 +97,3 @@ func init() {
}, },
}) })
} }
type FloatRandom struct {
BitSize int
Min float64
Max float64
config config.ProcessorConfig
}
func (fr *FloatRandom) Process(ctx context.Context, wrappedPayload common.WrappedPayload) (common.WrappedPayload, error) {
if fr.BitSize == 32 {
payloadFloat := rand.Float32()*(float32(fr.Max)-float32(fr.Min)) + float32(fr.Min)
wrappedPayload.Payload = payloadFloat
return wrappedPayload, nil
}
if fr.BitSize == 64 {
payloadFloat := rand.Float64()*(fr.Max-fr.Min) + fr.Min
wrappedPayload.Payload = payloadFloat
return wrappedPayload, nil
}
wrappedPayload.End = true
return wrappedPayload, errors.New("float.random bitSize error: must be 32 or 64")
}
func (fr *FloatRandom) Type() string {
return fr.config.Type
}
+175 -184
View File
@@ -13,190 +13,6 @@ import (
"github.com/jwetzell/showbridge-go/internal/config" "github.com/jwetzell/showbridge-go/internal/config"
) )
func init() {
RegisterProcessor(ProcessorRegistration{
Type: "freed.create",
Title: "Create FreeD",
ParamsSchema: &jsonschema.Schema{
Type: "object",
Properties: map[string]*jsonschema.Schema{
"id": {
Title: "Camera ID",
Description: "ID of the camera this FreeD position corresponds to",
Type: "string",
},
"pan": {
Title: "Pan",
Description: "pan angle of the camera",
Type: "string",
},
"tilt": {
Title: "Tilt",
Description: "tilt angle of the camera",
Type: "string",
},
"roll": {
Title: "Roll",
Description: "roll angle of the camera",
Type: "string",
},
"posX": {
Title: "Position X",
Description: "X coordinate of the camera's position",
Type: "string",
},
"posY": {
Title: "Position Y",
Description: "Y coordinate of the camera's position",
Type: "string",
},
"posZ": {
Title: "Position Z",
Description: "Z coordinate of the camera's position",
Type: "string",
},
"zoom": {
Title: "Zoom",
Description: "zoom level of the camera",
Type: "string",
},
"focus": {
Title: "Focus",
Description: "focus level of the camera",
Type: "string",
},
},
Required: []string{
"id",
"pan",
"tilt",
"roll",
"posX",
"posY",
"posZ",
"zoom",
"focus",
},
AdditionalProperties: &jsonschema.Schema{Not: &jsonschema.Schema{}},
},
New: func(config config.ProcessorConfig) (Processor, error) {
// TODO(jwetzell): make some params optional
params := config.Params
idString, err := params.GetString("id")
if err != nil {
return nil, fmt.Errorf("freed.create id error: %w", err)
}
idTemplate, err := template.New("id").Parse(idString)
if err != nil {
return nil, err
}
panString, err := params.GetString("pan")
if err != nil {
return nil, fmt.Errorf("freed.create pan error: %w", err)
}
panTemplate, err := template.New("pan").Parse(panString)
if err != nil {
return nil, err
}
tiltString, err := params.GetString("tilt")
if err != nil {
return nil, fmt.Errorf("freed.create tilt error: %w", err)
}
tiltTemplate, err := template.New("tilt").Parse(tiltString)
if err != nil {
return nil, err
}
rollString, err := params.GetString("roll")
if err != nil {
return nil, fmt.Errorf("freed.create roll error: %w", err)
}
rollTemplate, err := template.New("roll").Parse(rollString)
if err != nil {
return nil, err
}
posXString, err := params.GetString("posX")
if err != nil {
return nil, fmt.Errorf("freed.create posX error: %w", err)
}
posXTemplate, err := template.New("posX").Parse(posXString)
if err != nil {
return nil, err
}
posYString, err := params.GetString("posY")
if err != nil {
return nil, fmt.Errorf("freed.create posY error: %w", err)
}
posYTemplate, err := template.New("posY").Parse(posYString)
if err != nil {
return nil, err
}
posZString, err := params.GetString("posZ")
if err != nil {
return nil, fmt.Errorf("freed.create posZ error: %w", err)
}
posZTemplate, err := template.New("posZ").Parse(posZString)
if err != nil {
return nil, err
}
zoomString, err := params.GetString("zoom")
if err != nil {
return nil, fmt.Errorf("freed.create zoom error: %w", err)
}
zoomTemplate, err := template.New("zoom").Parse(zoomString)
if err != nil {
return nil, err
}
focusString, err := params.GetString("focus")
if err != nil {
return nil, fmt.Errorf("freed.create focus error: %w", err)
}
focusTemplate, err := template.New("focus").Parse(focusString)
if err != nil {
return nil, err
}
return &FreeDCreate{
config: config,
Id: idTemplate,
Pan: panTemplate,
Tilt: tiltTemplate,
Roll: rollTemplate,
PosX: posXTemplate,
PosY: posYTemplate,
PosZ: posZTemplate,
Zoom: zoomTemplate,
Focus: focusTemplate,
}, nil
},
})
}
type FreeDCreate struct { type FreeDCreate struct {
config config.ProcessorConfig config config.ProcessorConfig
Id *template.Template Id *template.Template
@@ -387,3 +203,178 @@ func (fc *FreeDCreate) Process(ctx context.Context, wrappedPayload common.Wrappe
func (fc *FreeDCreate) Type() string { func (fc *FreeDCreate) Type() string {
return fc.config.Type return fc.config.Type
} }
func init() {
RegisterProcessor(ProcessorRegistration{
Type: "freed.create",
Title: "Create FreeD",
ParamsSchema: &jsonschema.Schema{
Type: "object",
Properties: map[string]*jsonschema.Schema{
"id": {
Title: "Camera ID",
Type: "string",
},
"pan": {
Title: "Pan",
Type: "string",
},
"tilt": {
Title: "Tilt",
Type: "string",
},
"roll": {
Title: "Roll",
Type: "string",
},
"posX": {
Title: "Position X",
Type: "string",
},
"posY": {
Title: "Position Y",
Type: "string",
},
"posZ": {
Title: "Position Z",
Type: "string",
},
"zoom": {
Title: "Zoom",
Type: "string",
},
"focus": {
Title: "Focus",
Type: "string",
},
},
Required: []string{
"id",
"pan",
"tilt",
"roll",
"posX",
"posY",
"posZ",
"zoom",
"focus",
},
AdditionalProperties: nil,
},
New: func(config config.ProcessorConfig) (Processor, error) {
// TODO(jwetzell): make some params optional
params := config.Params
idString, err := params.GetString("id")
if err != nil {
return nil, fmt.Errorf("freed.create id error: %w", err)
}
idTemplate, err := template.New("id").Parse(idString)
if err != nil {
return nil, err
}
panString, err := params.GetString("pan")
if err != nil {
return nil, fmt.Errorf("freed.create pan error: %w", err)
}
panTemplate, err := template.New("pan").Parse(panString)
if err != nil {
return nil, err
}
tiltString, err := params.GetString("tilt")
if err != nil {
return nil, fmt.Errorf("freed.create tilt error: %w", err)
}
tiltTemplate, err := template.New("tilt").Parse(tiltString)
if err != nil {
return nil, err
}
rollString, err := params.GetString("roll")
if err != nil {
return nil, fmt.Errorf("freed.create roll error: %w", err)
}
rollTemplate, err := template.New("roll").Parse(rollString)
if err != nil {
return nil, err
}
posXString, err := params.GetString("posX")
if err != nil {
return nil, fmt.Errorf("freed.create posX error: %w", err)
}
posXTemplate, err := template.New("posX").Parse(posXString)
if err != nil {
return nil, err
}
posYString, err := params.GetString("posY")
if err != nil {
return nil, fmt.Errorf("freed.create posY error: %w", err)
}
posYTemplate, err := template.New("posY").Parse(posYString)
if err != nil {
return nil, err
}
posZString, err := params.GetString("posZ")
if err != nil {
return nil, fmt.Errorf("freed.create posZ error: %w", err)
}
posZTemplate, err := template.New("posZ").Parse(posZString)
if err != nil {
return nil, err
}
zoomString, err := params.GetString("zoom")
if err != nil {
return nil, fmt.Errorf("freed.create zoom error: %w", err)
}
zoomTemplate, err := template.New("zoom").Parse(zoomString)
if err != nil {
return nil, err
}
focusString, err := params.GetString("focus")
if err != nil {
return nil, fmt.Errorf("freed.create focus error: %w", err)
}
focusTemplate, err := template.New("focus").Parse(focusString)
if err != nil {
return nil, err
}
return &FreeDCreate{
config: config,
Id: idTemplate,
Pan: panTemplate,
Tilt: tiltTemplate,
Roll: rollTemplate,
PosX: posXTemplate,
PosY: posYTemplate,
PosZ: posZTemplate,
Zoom: zoomTemplate,
Focus: focusTemplate,
}, nil
},
})
}
+12 -20
View File
@@ -9,21 +9,10 @@ import (
"github.com/jwetzell/showbridge-go/internal/config" "github.com/jwetzell/showbridge-go/internal/config"
) )
func init() {
RegisterProcessor(ProcessorRegistration{
Type: "freed.decode",
Title: "Decode FreeD",
New: func(config config.ProcessorConfig) (Processor, error) {
return &FreeDDecode{config: config}, nil
},
})
}
type FreeDDecode struct { type FreeDDecode struct {
config config.ProcessorConfig config config.ProcessorConfig
buf [29]byte
} }
func (fd *FreeDDecode) Process(ctx context.Context, wrappedPayload common.WrappedPayload) (common.WrappedPayload, error) { func (fd *FreeDDecode) Process(ctx context.Context, wrappedPayload common.WrappedPayload) (common.WrappedPayload, error) {
payload := wrappedPayload.Payload payload := wrappedPayload.Payload
payloadBytes, ok := common.GetAnyAsByteSlice(payload) payloadBytes, ok := common.GetAnyAsByteSlice(payload)
@@ -33,14 +22,7 @@ func (fd *FreeDDecode) Process(ctx context.Context, wrappedPayload common.Wrappe
return wrappedPayload, errors.New("freed.decode processor only accepts a []byte") return wrappedPayload, errors.New("freed.decode processor only accepts a []byte")
} }
if len(payloadBytes) != 29 { payloadMessage, err := freeD.Decode(payloadBytes)
wrappedPayload.End = true
return wrappedPayload, errors.New("freeD packet must be exactly 29 bytes")
}
copy(fd.buf[:], payloadBytes)
payloadMessage, err := freeD.Decode(fd.buf)
if err != nil { if err != nil {
wrappedPayload.End = true wrappedPayload.End = true
return wrappedPayload, err return wrappedPayload, err
@@ -52,3 +34,13 @@ func (fd *FreeDDecode) Process(ctx context.Context, wrappedPayload common.Wrappe
func (fd *FreeDDecode) Type() string { func (fd *FreeDDecode) Type() string {
return fd.config.Type return fd.config.Type
} }
func init() {
RegisterProcessor(ProcessorRegistration{
Type: "freed.decode",
Title: "Decode FreeD",
New: func(config config.ProcessorConfig) (Processor, error) {
return &FreeDDecode{config: config}, nil
},
})
}
+10 -10
View File
@@ -9,16 +9,6 @@ import (
"github.com/jwetzell/showbridge-go/internal/config" "github.com/jwetzell/showbridge-go/internal/config"
) )
func init() {
RegisterProcessor(ProcessorRegistration{
Type: "freed.encode",
Title: "Encode FreeD",
New: func(config config.ProcessorConfig) (Processor, error) {
return &FreeDEncode{config: config}, nil
},
})
}
type FreeDEncode struct { type FreeDEncode struct {
config config.ProcessorConfig config config.ProcessorConfig
} }
@@ -41,3 +31,13 @@ func (fe *FreeDEncode) Process(ctx context.Context, wrappedPayload common.Wrappe
func (fe *FreeDEncode) Type() string { func (fe *FreeDEncode) Type() string {
return fe.config.Type return fe.config.Type
} }
func init() {
RegisterProcessor(ProcessorRegistration{
Type: "freed.encode",
Title: "Encode FreeD",
New: func(config config.ProcessorConfig) (Processor, error) {
return &FreeDEncode{config: config}, nil
},
})
}
+52 -49
View File
@@ -14,55 +14,6 @@ import (
"github.com/jwetzell/showbridge-go/internal/config" "github.com/jwetzell/showbridge-go/internal/config"
) )
func init() {
RegisterProcessor(ProcessorRegistration{
Type: "http.request.do",
Title: "Do HTTP Request",
ParamsSchema: &jsonschema.Schema{
Type: "object",
Properties: map[string]*jsonschema.Schema{
"method": {
Title: "HTTP Method",
Description: "method to use for the HTTP request",
Type: "string",
Enum: []any{"GET", "POST", "PUT", "PATCH", "DELETE"},
},
"url": {
Title: "URL",
Description: "URL to send the HTTP request to",
Type: "string",
},
},
Required: []string{"method", "url"},
AdditionalProperties: &jsonschema.Schema{Not: &jsonschema.Schema{}},
},
New: func(config config.ProcessorConfig) (Processor, error) {
params := config.Params
methodString, err := params.GetString("method")
if err != nil {
return nil, fmt.Errorf("http.request.do method error: %w", err)
}
urlString, err := params.GetString("url")
if err != nil {
return nil, fmt.Errorf("http.request.do url error: %w", err)
}
urlTemplate, err := template.New("url").Parse(urlString)
if err != nil {
return nil, err
}
client := &http.Client{
Timeout: 10 * time.Second,
}
return &HTTPRequestDo{config: config, URL: urlTemplate, Method: methodString, client: client}, nil
},
})
}
type HTTPRequestDo struct { type HTTPRequestDo struct {
config config.ProcessorConfig config config.ProcessorConfig
client *http.Client client *http.Client
@@ -70,6 +21,11 @@ type HTTPRequestDo struct {
URL *template.Template URL *template.Template
} }
type HTTPResponse struct {
Status int
Body []byte
}
func (hrd *HTTPRequestDo) Process(ctx context.Context, wrappedPayload common.WrappedPayload) (common.WrappedPayload, error) { func (hrd *HTTPRequestDo) Process(ctx context.Context, wrappedPayload common.WrappedPayload) (common.WrappedPayload, error) {
templateData := wrappedPayload templateData := wrappedPayload
@@ -117,3 +73,50 @@ func (hrd *HTTPRequestDo) Process(ctx context.Context, wrappedPayload common.Wra
func (hrd *HTTPRequestDo) Type() string { func (hrd *HTTPRequestDo) Type() string {
return hrd.config.Type return hrd.config.Type
} }
func init() {
RegisterProcessor(ProcessorRegistration{
Type: "http.request.do",
Title: "Do HTTP Request",
ParamsSchema: &jsonschema.Schema{
Type: "object",
Properties: map[string]*jsonschema.Schema{
"method": {
Title: "HTTP Method",
Type: "string",
Enum: []any{"GET", "POST", "PUT", "PATCH", "DELETE"},
},
"url": {
Title: "URL",
Type: "string",
},
},
Required: []string{"method", "url"},
AdditionalProperties: nil,
},
New: func(config config.ProcessorConfig) (Processor, error) {
params := config.Params
methodString, err := params.GetString("method")
if err != nil {
return nil, fmt.Errorf("http.request.do method error: %w", err)
}
urlString, err := params.GetString("url")
if err != nil {
return nil, fmt.Errorf("http.request.do url error: %w", err)
}
urlTemplate, err := template.New("url").Parse(urlString)
if err != nil {
return nil, err
}
client := &http.Client{
Timeout: 10 * time.Second,
}
return &HTTPRequestDo{config: config, URL: urlTemplate, Method: methodString, client: client}, nil
},
})
}
+36 -41
View File
@@ -1,3 +1,5 @@
//go:build !js
package processor package processor
import ( import (
@@ -11,6 +13,33 @@ import (
"github.com/jwetzell/showbridge-go/internal/config" "github.com/jwetzell/showbridge-go/internal/config"
) )
type HTTPResponseCreate struct {
Status int
BodyTmpl *template.Template
config config.ProcessorConfig
}
func (hrc *HTTPResponseCreate) Process(ctx context.Context, wrappedPayload common.WrappedPayload) (common.WrappedPayload, error) {
templateData := wrappedPayload
var bodyBuffer bytes.Buffer
err := hrc.BodyTmpl.Execute(&bodyBuffer, templateData)
if err != nil {
wrappedPayload.End = true
return wrappedPayload, err
}
wrappedPayload.Payload = HTTPResponse{
Status: hrc.Status,
Body: bodyBuffer.Bytes(),
}
return wrappedPayload, nil
}
func (hrc *HTTPResponseCreate) Type() string {
return hrc.config.Type
}
func init() { func init() {
RegisterProcessor(ProcessorRegistration{ RegisterProcessor(ProcessorRegistration{
Type: "http.response.create", Type: "http.response.create",
@@ -19,18 +48,16 @@ func init() {
Type: "object", Type: "object",
Properties: map[string]*jsonschema.Schema{ Properties: map[string]*jsonschema.Schema{
"status": { "status": {
Title: "Status Code", Title: "Status Code",
Description: "status code to set on the response", Type: "integer",
Type: "integer",
}, },
"bodyTemplate": { "body": {
Title: "Body Template", Title: "Body",
Description: "template for the response body", Type: "string",
Type: "string",
}, },
}, },
Required: []string{"status", "bodyTemplate"}, Required: []string{"status", "body"},
AdditionalProperties: &jsonschema.Schema{Not: &jsonschema.Schema{}}, AdditionalProperties: nil,
}, },
New: func(config config.ProcessorConfig) (Processor, error) { New: func(config config.ProcessorConfig) (Processor, error) {
params := config.Params params := config.Params
@@ -56,35 +83,3 @@ func init() {
}, },
}) })
} }
type HTTPResponseCreate struct {
Status int
BodyTmpl *template.Template
config config.ProcessorConfig
}
type HTTPResponse struct {
Status int
Body []byte
}
func (hrc *HTTPResponseCreate) Process(ctx context.Context, wrappedPayload common.WrappedPayload) (common.WrappedPayload, error) {
templateData := wrappedPayload
var bodyBuffer bytes.Buffer
err := hrc.BodyTmpl.Execute(&bodyBuffer, templateData)
if err != nil {
wrappedPayload.End = true
return wrappedPayload, err
}
wrappedPayload.Payload = HTTPResponse{
Status: hrc.Status,
Body: bodyBuffer.Bytes(),
}
return wrappedPayload, nil
}
func (hrc *HTTPResponseCreate) Type() string {
return hrc.config.Type
}
+47 -49
View File
@@ -12,55 +12,6 @@ import (
"github.com/jwetzell/showbridge-go/internal/config" "github.com/jwetzell/showbridge-go/internal/config"
) )
func init() {
RegisterProcessor(ProcessorRegistration{
Type: "int.parse",
Title: "Parse Int",
ParamsSchema: &jsonschema.Schema{
Type: "object",
Properties: map[string]*jsonschema.Schema{
"base": {
Title: "Base",
Description: "numerical base to use for parsing the integer",
Type: "integer",
Enum: []any{0, 2, 8, 10, 16},
Default: json.RawMessage("10"),
},
"bitSize": {
Title: "Bit Size",
Description: "bit size of the resulting integer",
Type: "integer",
Enum: []any{0, 8, 16, 32, 64},
Default: json.RawMessage("64"),
},
},
AdditionalProperties: &jsonschema.Schema{Not: &jsonschema.Schema{}},
},
New: func(moduleConfig config.ProcessorConfig) (Processor, error) {
params := moduleConfig.Params
baseNum, err := params.GetInt("base")
if err != nil {
if errors.Is(err, config.ErrParamNotFound) {
baseNum = 10
} else {
return nil, fmt.Errorf("int.parse base error: %w", err)
}
}
bitSizeNum, err := params.GetInt("bitSize")
if err != nil {
if errors.Is(err, config.ErrParamNotFound) {
bitSizeNum = 64
} else {
return nil, fmt.Errorf("int.parse bitSize error: %w", err)
}
}
return &IntParse{config: moduleConfig, Base: baseNum, BitSize: bitSizeNum}, nil
},
})
}
type IntParse struct { type IntParse struct {
Base int Base int
BitSize int BitSize int
@@ -88,3 +39,50 @@ func (ip *IntParse) Process(ctx context.Context, wrappedPayload common.WrappedPa
func (ip *IntParse) Type() string { func (ip *IntParse) Type() string {
return ip.config.Type return ip.config.Type
} }
func init() {
RegisterProcessor(ProcessorRegistration{
Type: "int.parse",
Title: "Parse Int",
ParamsSchema: &jsonschema.Schema{
Type: "object",
Properties: map[string]*jsonschema.Schema{
"base": {
Title: "Base",
Type: "integer",
Enum: []any{0, 2, 8, 10, 16},
Default: json.RawMessage("10"),
},
"bitSize": {
Title: "Bit Size",
Type: "integer",
Enum: []any{0, 8, 16, 32, 64},
Default: json.RawMessage("64"),
},
},
AdditionalProperties: nil,
},
New: func(moduleConfig config.ProcessorConfig) (Processor, error) {
params := moduleConfig.Params
baseNum, err := params.GetInt("base")
if err != nil {
if errors.Is(err, config.ErrParamNotFound) {
baseNum = 10
} else {
return nil, fmt.Errorf("int.parse base error: %w", err)
}
}
bitSizeNum, err := params.GetInt("bitSize")
if err != nil {
if errors.Is(err, config.ErrParamNotFound) {
bitSizeNum = 64
} else {
return nil, fmt.Errorf("int.parse bitSize error: %w", err)
}
}
return &IntParse{config: moduleConfig, Base: baseNum, BitSize: bitSizeNum}, nil
},
})
}
+21 -23
View File
@@ -11,6 +11,22 @@ import (
"github.com/jwetzell/showbridge-go/internal/config" "github.com/jwetzell/showbridge-go/internal/config"
) )
type IntRandom struct {
Min int
Max int
config config.ProcessorConfig
}
func (ir *IntRandom) Process(ctx context.Context, wrappedPayload common.WrappedPayload) (common.WrappedPayload, error) {
payloadInt := rand.IntN(ir.Max-ir.Min+1) + ir.Min
wrappedPayload.Payload = payloadInt
return wrappedPayload, nil
}
func (ir *IntRandom) Type() string {
return ir.config.Type
}
func init() { func init() {
RegisterProcessor(ProcessorRegistration{ RegisterProcessor(ProcessorRegistration{
Type: "int.random", Type: "int.random",
@@ -19,18 +35,16 @@ func init() {
Type: "object", Type: "object",
Properties: map[string]*jsonschema.Schema{ Properties: map[string]*jsonschema.Schema{
"min": { "min": {
Title: "Minimum", Title: "Minimum",
Description: "inclusive minimum value of the random integer", Type: "integer",
Type: "integer",
}, },
"max": { "max": {
Title: "Maximum", Title: "Maximum",
Description: "inclusive maximum value of the random integer", Type: "integer",
Type: "integer",
}, },
}, },
Required: []string{"min", "max"}, Required: []string{"min", "max"},
AdditionalProperties: &jsonschema.Schema{Not: &jsonschema.Schema{}}, AdditionalProperties: nil,
}, },
New: func(config config.ProcessorConfig) (Processor, error) { New: func(config config.ProcessorConfig) (Processor, error) {
params := config.Params params := config.Params
@@ -53,19 +67,3 @@ func init() {
}, },
}) })
} }
type IntRandom struct {
Min int
Max int
config config.ProcessorConfig
}
func (ir *IntRandom) Process(ctx context.Context, wrappedPayload common.WrappedPayload) (common.WrappedPayload, error) {
payloadInt := rand.IntN(ir.Max-ir.Min+1) + ir.Min
wrappedPayload.Payload = payloadInt
return wrappedPayload, nil
}
func (ir *IntRandom) Type() string {
return ir.config.Type
}
+34 -38
View File
@@ -10,6 +10,31 @@ import (
"github.com/jwetzell/showbridge-go/internal/config" "github.com/jwetzell/showbridge-go/internal/config"
) )
type IntScale struct {
OutMin int
OutMax int
InMin int
InMax int
config config.ProcessorConfig
}
func (ir *IntScale) Process(ctx context.Context, wrappedPayload common.WrappedPayload) (common.WrappedPayload, error) {
payload := wrappedPayload.Payload
payloadInt, ok := common.GetAnyAs[int](payload)
if !ok {
wrappedPayload.End = true
return wrappedPayload, errors.New("int.scale can only process an int")
}
payloadInt = (payloadInt-ir.InMin)*(ir.OutMax-ir.OutMin)/(ir.InMax-ir.InMin) + ir.OutMin
wrappedPayload.Payload = payloadInt
return wrappedPayload, nil
}
func (ir *IntScale) Type() string {
return ir.config.Type
}
func init() { func init() {
RegisterProcessor(ProcessorRegistration{ RegisterProcessor(ProcessorRegistration{
Type: "int.scale", Type: "int.scale",
@@ -18,28 +43,24 @@ func init() {
Type: "object", Type: "object",
Properties: map[string]*jsonschema.Schema{ Properties: map[string]*jsonschema.Schema{
"inMin": { "inMin": {
Title: "Input Minimum", Title: "Input Minimum",
Description: "minimum value of the input integer", Type: "integer",
Type: "integer",
}, },
"inMax": { "inMax": {
Title: "Input Maximum", Title: "Input Maximum",
Description: "maximum value of the input integer", Type: "integer",
Type: "integer",
}, },
"outMin": { "outMin": {
Title: "Output Minimum", Title: "Output Minimum",
Description: "minimum value of the output integer", Type: "integer",
Type: "integer",
}, },
"outMax": { "outMax": {
Title: "Output Maximum", Title: "Output Maximum",
Description: "maximum value of the output integer", Type: "integer",
Type: "integer",
}, },
}, },
Required: []string{"inMin", "inMax", "outMin", "outMax"}, Required: []string{"inMin", "inMax", "outMin", "outMax"},
AdditionalProperties: &jsonschema.Schema{Not: &jsonschema.Schema{}}, AdditionalProperties: nil,
}, },
New: func(config config.ProcessorConfig) (Processor, error) { New: func(config config.ProcessorConfig) (Processor, error) {
params := config.Params params := config.Params
@@ -76,28 +97,3 @@ func init() {
}, },
}) })
} }
type IntScale struct {
OutMin int
OutMax int
InMin int
InMax int
config config.ProcessorConfig
}
func (is *IntScale) Process(ctx context.Context, wrappedPayload common.WrappedPayload) (common.WrappedPayload, error) {
payload := wrappedPayload.Payload
payloadInt, ok := common.GetAnyAs[int](payload)
if !ok {
wrappedPayload.End = true
return wrappedPayload, errors.New("int.scale can only process an int")
}
payloadInt = (payloadInt-is.InMin)*(is.OutMax-is.OutMin)/(is.InMax-is.InMin) + is.OutMin
wrappedPayload.Payload = payloadInt
return wrappedPayload, nil
}
func (is *IntScale) Type() string {
return is.config.Type
}
+10 -10
View File
@@ -9,16 +9,6 @@ import (
"github.com/jwetzell/showbridge-go/internal/config" "github.com/jwetzell/showbridge-go/internal/config"
) )
func init() {
RegisterProcessor(ProcessorRegistration{
Type: "json.decode",
Title: "Decode JSON",
New: func(config config.ProcessorConfig) (Processor, error) {
return &JsonDecode{config: config}, nil
},
})
}
type JsonDecode struct { type JsonDecode struct {
config config.ProcessorConfig config config.ProcessorConfig
} }
@@ -53,3 +43,13 @@ func (jd *JsonDecode) Process(ctx context.Context, wrappedPayload common.Wrapped
func (jd *JsonDecode) Type() string { func (jd *JsonDecode) Type() string {
return jd.config.Type return jd.config.Type
} }
func init() {
RegisterProcessor(ProcessorRegistration{
Type: "json.decode",
Title: "Decode JSON",
New: func(config config.ProcessorConfig) (Processor, error) {
return &JsonDecode{config: config}, nil
},
})
}

Some files were not shown because too many files have changed in this diff Show More