refactor: client/server

This commit is contained in:
Rob Watson 2025-05-06 20:17:41 +02:00
parent d4e136c8d6
commit c556a30317
35 changed files with 5005 additions and 433 deletions

10
go.mod
View File

@ -11,7 +11,11 @@ require (
github.com/rivo/tview v0.0.0-20250330220935-949945f8d922
github.com/stretchr/testify v1.10.0
github.com/testcontainers/testcontainers-go v0.35.0
github.com/urfave/cli/v2 v2.27.6
golang.design/x/clipboard v0.7.0
golang.org/x/sync v0.13.0
google.golang.org/grpc v1.69.4
google.golang.org/protobuf v1.36.6
gopkg.in/yaml.v3 v3.0.1
)
@ -24,6 +28,7 @@ require (
github.com/containerd/log v0.1.0 // indirect
github.com/containerd/platforms v0.2.1 // indirect
github.com/cpuguy83/dockercfg v0.3.2 // indirect
github.com/cpuguy83/go-md2man/v2 v2.0.5 // indirect
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
github.com/distribution/reference v0.6.0 // indirect
github.com/docker/go-units v0.5.0 // indirect
@ -65,6 +70,7 @@ require (
github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c // indirect
github.com/rivo/uniseg v0.4.7 // indirect
github.com/rs/zerolog v1.33.0 // indirect
github.com/russross/blackfriday/v2 v2.1.0 // indirect
github.com/sagikazarmark/locafero v0.7.0 // indirect
github.com/sagikazarmark/slog-shim v0.1.0 // indirect
github.com/shirou/gopsutil/v3 v3.23.12 // indirect
@ -81,6 +87,7 @@ require (
github.com/tklauser/go-sysconf v0.3.12 // indirect
github.com/tklauser/numcpus v0.6.1 // indirect
github.com/vektra/mockery/v2 v2.52.2 // indirect
github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1 // indirect
github.com/yusufpapurcu/wmi v1.2.3 // indirect
go.opentelemetry.io/auto/sdk v1.1.0 // indirect
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.60.0 // indirect
@ -95,12 +102,13 @@ require (
golang.org/x/image v0.26.0 // indirect
golang.org/x/mobile v0.0.0-20250408133729-978277e7eaf7 // indirect
golang.org/x/mod v0.24.0 // indirect
golang.org/x/sync v0.13.0 // indirect
golang.org/x/net v0.39.0 // indirect
golang.org/x/sys v0.32.0 // indirect
golang.org/x/term v0.31.0 // indirect
golang.org/x/text v0.24.0 // indirect
golang.org/x/time v0.9.0 // indirect
golang.org/x/tools v0.32.0 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20250115164207-1a7da9e5054f // indirect
gopkg.in/ini.v1 v1.67.0 // indirect
)

13
go.sum
View File

@ -18,6 +18,8 @@ github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSV
github.com/cpuguy83/dockercfg v0.3.2 h1:DlJTyZGBDlXqUZ2Dk2Q3xHs/FtnooJJVaad2S9GKorA=
github.com/cpuguy83/dockercfg v0.3.2/go.mod h1:sugsbF4//dDlL/i+S+rtpIWp+5h0BHJHfjj5/jFyUJc=
github.com/cpuguy83/go-md2man/v2 v2.0.4/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
github.com/cpuguy83/go-md2man/v2 v2.0.5 h1:ZtcqGrnekaHpVLArFSe4HK5DoKx1T0rq2DwVB0alcyc=
github.com/cpuguy83/go-md2man/v2 v2.0.5/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
github.com/creack/pty v1.1.18 h1:n56/Zwd5o6whRC5PMGretI4IdRLlmBXYNjScPaBgsbY=
github.com/creack/pty v1.1.18/go.mod h1:MOBLtS5ELjhRRrroQr9kyvTxUAFNvYEK993ew/Vr4O4=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
@ -52,6 +54,8 @@ github.com/go-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiU
github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q=
github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q=
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.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
@ -140,6 +144,7 @@ github.com/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWN
github.com/rs/xid v1.5.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg=
github.com/rs/zerolog v1.33.0 h1:1cU2KZkvPxNyfgEmhHAz/1A9Bz+llsdYzklWFzgp0r8=
github.com/rs/zerolog v1.33.0/go.mod h1:/7mN4D5sKwJLZQ2b/znpjC3/GQWY/xaDXUM0kKWRHss=
github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk=
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/sagikazarmark/locafero v0.7.0 h1:5MqpDsTGNDhY8sGp0Aowyf0qKsPrhewaLSsFaodPcyo=
github.com/sagikazarmark/locafero v0.7.0/go.mod h1:2za3Cg5rMaTMoG/2Ulr9AwtFaIppKXTRYnozin4aB5k=
@ -185,8 +190,12 @@ github.com/tklauser/go-sysconf v0.3.12 h1:0QaGUFOdQaIVdPgfITYzaTegZvdCjmYO52cSFA
github.com/tklauser/go-sysconf v0.3.12/go.mod h1:Ho14jnntGE1fpdOqQEEaiKRpvIavV0hSfmBq8nJbHYI=
github.com/tklauser/numcpus v0.6.1 h1:ng9scYS7az0Bk4OZLvrNXNSAO2Pxr1XXRAPyjhIx+Fk=
github.com/tklauser/numcpus v0.6.1/go.mod h1:1XfjsgE2zo8GVw7POkMbHENHzVg3GzmoZ9fESEdAacY=
github.com/urfave/cli/v2 v2.27.6 h1:VdRdS98FNhKZ8/Az8B7MTyGQmpIr36O1EHybx/LaZ4g=
github.com/urfave/cli/v2 v2.27.6/go.mod h1:3Sevf16NykTbInEnD0yKkjDAeZDS0A6bzhBH5hrMvTQ=
github.com/vektra/mockery/v2 v2.52.2 h1:8QfPKUIrq8P3Cs7G79Iu4Byd5wdhGCE0quIS27x7rQo=
github.com/vektra/mockery/v2 v2.52.2/go.mod h1:zGDY/f6bip0Yh13GQ5j7xa43fuEoYBa4ICHEaihisHw=
github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1 h1:gEOO8jv9F4OT7lGCjxCBTO/36wtF6j2nSip77qHd4x4=
github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1/go.mod h1:Ohn+xnUBiLI6FVj/9LpzZWtj1/D6lUovWYBkxHVV3aM=
github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
@ -334,8 +343,8 @@ google.golang.org/genproto/googleapis/rpc v0.0.0-20250115164207-1a7da9e5054f h1:
google.golang.org/genproto/googleapis/rpc v0.0.0-20250115164207-1a7da9e5054f/go.mod h1:+2Yz8+CLJbIfL9z73EW45avw8Lmge3xVElCP9zEKi50=
google.golang.org/grpc v1.69.4 h1:MF5TftSMkd8GLw/m0KM6V8CMOCY6NZ1NQDPGFgbTt4A=
google.golang.org/grpc v1.69.4/go.mod h1:vyjdE6jLBI76dgpDojsFGNaHlxdjXN9ghpnd2o7JGZ4=
google.golang.org/protobuf v1.36.3 h1:82DV7MYdb8anAVi3qge1wSnMDrnKK7ebr+I0hHRN1BU=
google.golang.org/protobuf v1.36.3/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE=
google.golang.org/protobuf v1.36.6 h1:z1NpPI8ku2WgiWnf+t9wTPsn6eP1L7ksHUlkfLvd9xY=
google.golang.org/protobuf v1.36.6/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/5YcXBHnY=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=

View File

@ -0,0 +1,131 @@
package client
import (
"context"
"fmt"
"log/slog"
"git.netflux.io/rob/octoplex/internal/domain"
"git.netflux.io/rob/octoplex/internal/event"
pb "git.netflux.io/rob/octoplex/internal/generated/grpc"
"git.netflux.io/rob/octoplex/internal/protocol"
"git.netflux.io/rob/octoplex/internal/terminal"
"golang.org/x/sync/errgroup"
"google.golang.org/grpc"
"google.golang.org/grpc/credentials/insecure"
)
// App is the client application.
type App struct {
bus *event.Bus
clipboardAvailable bool
buildInfo domain.BuildInfo
screen *terminal.Screen
logger *slog.Logger
}
// NewParams contains the parameters for the App.
type NewParams struct {
ClipboardAvailable bool
BuildInfo domain.BuildInfo
Screen *terminal.Screen
Logger *slog.Logger
}
// New creates a new App instance.
func New(params NewParams) *App {
return &App{
bus: event.NewBus(params.Logger),
clipboardAvailable: params.ClipboardAvailable,
buildInfo: params.BuildInfo,
screen: params.Screen,
logger: params.Logger,
}
}
// Run starts the application, and blocks until it is closed.
//
// It returns nil if the application was closed by the user, or an error if it
// closed for any other reason.
func (a *App) Run(ctx context.Context) error {
g, ctx := errgroup.WithContext(ctx)
conn, err := grpc.NewClient("localhost:50051", grpc.WithTransportCredentials(insecure.NewCredentials()))
if err != nil {
return fmt.Errorf("connect to gRPC server: %w", err)
}
apiClient := pb.NewInternalAPIClient(conn)
stream, err := apiClient.Communicate(ctx)
if err != nil {
return fmt.Errorf("create gRPC stream: %w", err)
}
ui, err := terminal.NewUI(ctx, terminal.Params{
EventBus: a.bus,
Dispatcher: func(cmd event.Command) {
a.logger.Debug("Command dispatched to gRPC stream", "cmd", cmd.Name())
if sendErr := stream.Send(&pb.Envelope{Payload: &pb.Envelope_Command{Command: protocol.CommandToProto(cmd)}}); sendErr != nil {
a.logger.Error("Error dispatching command to gRPC stream", "err", sendErr)
}
},
ClipboardAvailable: a.clipboardAvailable,
BuildInfo: a.buildInfo,
Screen: a.screen,
Logger: a.logger.With("component", "ui"),
})
if err != nil {
return fmt.Errorf("start terminal user interface: %w", err)
}
defer ui.Close()
g.Go(func() error { return ui.Run(ctx) })
// After the UI is available, perform a handshake with the server.
// Ordering is important here. We want to ensure that the UI is ready to
// react to events received from the server. Performing the handshake ensures
// the client has received at least one event.
if err := a.doHandshake(stream); err != nil {
return fmt.Errorf("do handshake: %w", err)
}
g.Go(func() error {
for {
envelope, recErr := stream.Recv()
if recErr != nil {
return fmt.Errorf("receive envelope: %w", recErr)
}
pbEvt := envelope.GetEvent()
if pbEvt == nil {
a.logger.Error("Received envelope without event")
continue
}
evt := protocol.EventFromProto(pbEvt)
a.logger.Debug("Received event from gRPC stream", "event", evt.EventName(), "payload", evt)
a.bus.Send(evt)
}
})
if err := g.Wait(); err == terminal.ErrUserClosed {
return nil
} else {
return fmt.Errorf("errgroup.Wait: %w", err)
}
}
func (a *App) doHandshake(stream pb.InternalAPI_CommunicateClient) error {
if err := stream.Send(&pb.Envelope{Payload: &pb.Envelope_Command{Command: &pb.Command{CommandType: &pb.Command_StartHandshake{}}}}); err != nil {
return fmt.Errorf("send start handshake command: %w", err)
}
env, err := stream.Recv()
if err != nil {
return fmt.Errorf("receive handshake completed event: %w", err)
}
if evt := env.GetEvent(); evt == nil || evt.GetHandshakeCompleted() == nil {
return fmt.Errorf("expected handshake completed event but got: %T", env)
}
return nil
}

View File

@ -1,9 +1,11 @@
//go:build integration
package app_test
package client_test
import (
"context"
"encoding/json"
"errors"
"fmt"
"io"
"log/slog"
@ -14,39 +16,79 @@ import (
"testing"
"time"
"git.netflux.io/rob/octoplex/internal/app"
"git.netflux.io/rob/octoplex/internal/client"
"git.netflux.io/rob/octoplex/internal/config"
"git.netflux.io/rob/octoplex/internal/container"
"git.netflux.io/rob/octoplex/internal/domain"
"git.netflux.io/rob/octoplex/internal/server"
"git.netflux.io/rob/octoplex/internal/terminal"
"github.com/gdamore/tcell/v2"
"github.com/stretchr/testify/require"
"github.com/testcontainers/testcontainers-go"
"golang.org/x/sync/errgroup"
)
func buildAppParams(
t *testing.T,
func buildClientServer(
configService *config.Service,
dockerClient container.DockerClient,
screen tcell.SimulationScreen,
screenCaptureC chan<- terminal.ScreenCapture,
logger *slog.Logger,
) app.Params {
t.Helper()
return app.Params{
ConfigService: configService,
DockerClient: dockerClient,
) (*client.App, *server.App) {
client := client.New(client.NewParams{
BuildInfo: domain.BuildInfo{Version: "0.0.1", GoVersion: "go1.16.3"},
Screen: &terminal.Screen{
Screen: screen,
Width: 180,
Height: 25,
CaptureC: screenCaptureC,
},
ClipboardAvailable: false,
BuildInfo: domain.BuildInfo{Version: "0.0.1", GoVersion: "go1.16.3"},
Logger: logger,
}
Logger: logger,
})
server := server.New(server.Params{
ConfigService: configService,
DockerClient: dockerClient,
WaitForClient: true,
Logger: logger,
})
return client, server
}
type clientServerResult struct {
errClient error
errServer error
}
func runClientServer(
ctx context.Context,
_ *testing.T,
clientApp *client.App,
serverApp *server.App,
) <-chan clientServerResult {
ch := make(chan clientServerResult, 1)
g, ctx := errgroup.WithContext(ctx)
var clientErr, srvErr error
g.Go(func() error {
srvErr = serverApp.Run(ctx)
return errors.New("server closed")
})
g.Go(func() error {
clientErr = clientApp.Run(ctx)
return errors.New("client closed")
})
go func() {
_ = g.Wait()
ch <- clientServerResult{errClient: clientErr, errServer: srvErr}
}()
return ch
}
func setupSimulationScreen(t *testing.T) (tcell.SimulationScreen, chan<- terminal.ScreenCapture, func() []string) {

View File

@ -1,6 +1,6 @@
//go:build integration
package app_test
package client_test
import (
"cmp"
@ -15,12 +15,10 @@ import (
"testing"
"time"
"git.netflux.io/rob/octoplex/internal/app"
"git.netflux.io/rob/octoplex/internal/config"
"git.netflux.io/rob/octoplex/internal/container"
"git.netflux.io/rob/octoplex/internal/container/mocks"
"git.netflux.io/rob/octoplex/internal/domain"
"git.netflux.io/rob/octoplex/internal/terminal"
"git.netflux.io/rob/octoplex/internal/testhelpers"
"github.com/docker/docker/api/types/network"
dockerclient "github.com/docker/docker/client"
@ -126,26 +124,8 @@ func testIntegration(t *testing.T, mediaServerConfig config.MediaServerSource) {
Destinations: []config.Destination{{Name: "Local server 1", URL: destURL1}},
})
done := make(chan struct{})
go func() {
defer func() {
done <- struct{}{}
}()
require.Equal(t, context.Canceled, app.New(app.Params{
ConfigService: configService,
DockerClient: dockerClient,
Screen: &terminal.Screen{
Screen: screen,
Width: 160,
Height: 25,
CaptureC: screenCaptureC,
},
ClipboardAvailable: false,
BuildInfo: domain.BuildInfo{Version: "0.0.1", GoVersion: "go1.16.3"},
Logger: logger,
}).Run(ctx))
}()
client, server := buildClientServer(configService, dockerClient, screen, screenCaptureC, logger)
ch := runClientServer(ctx, t, client, server)
require.EventuallyWithT(
t,
@ -286,13 +266,12 @@ func testIntegration(t *testing.T, mediaServerConfig config.MediaServerSource) {
printScreen(t, getContents, "After stopping the first destination")
// TODO:
// - Source error
// - Additional features (copy URL, etc.)
cancel()
<-done
result := <-ch
// May be a gRPC error, not context.Canceled:
assert.ErrorContains(t, result.errClient, "context canceled")
assert.ErrorIs(t, result.errServer, context.Canceled)
}
func TestIntegrationCustomHost(t *testing.T) {
@ -313,14 +292,8 @@ func TestIntegrationCustomHost(t *testing.T) {
})
screen, screenCaptureC, getContents := setupSimulationScreen(t)
done := make(chan struct{})
go func() {
defer func() {
done <- struct{}{}
}()
require.Equal(t, context.Canceled, app.New(buildAppParams(t, configService, dockerClient, screen, screenCaptureC, logger)).Run(ctx))
}()
client, server := buildClientServer(configService, dockerClient, screen, screenCaptureC, logger)
ch := runClientServer(ctx, t, client, server)
time.Sleep(time.Second)
sendKey(t, screen, tcell.KeyF1, ' ')
@ -360,7 +333,10 @@ func TestIntegrationCustomHost(t *testing.T) {
cancel()
<-done
result := <-ch
// May be a gRPC error, not context.Canceled:
assert.ErrorContains(t, result.errClient, "context canceled")
assert.ErrorIs(t, result.errServer, context.Canceled)
}
func TestIntegrationCustomTLSCerts(t *testing.T) {
@ -384,14 +360,8 @@ func TestIntegrationCustomTLSCerts(t *testing.T) {
})
screen, screenCaptureC, getContents := setupSimulationScreen(t)
done := make(chan struct{})
go func() {
defer func() {
done <- struct{}{}
}()
require.Equal(t, context.Canceled, app.New(buildAppParams(t, configService, dockerClient, screen, screenCaptureC, logger)).Run(ctx))
}()
client, server := buildClientServer(configService, dockerClient, screen, screenCaptureC, logger)
ch := runClientServer(ctx, t, client, server)
require.EventuallyWithT(
t,
@ -426,7 +396,9 @@ func TestIntegrationCustomTLSCerts(t *testing.T) {
cancel()
<-done
result := <-ch
assert.ErrorContains(t, result.errClient, "context canceled")
assert.ErrorIs(t, result.errServer, context.Canceled)
}
func TestIntegrationRestartDestination(t *testing.T) {
@ -465,14 +437,8 @@ func TestIntegrationRestartDestination(t *testing.T) {
}},
})
done := make(chan struct{})
go func() {
defer func() {
done <- struct{}{}
}()
require.Equal(t, context.Canceled, app.New(buildAppParams(t, configService, dockerClient, screen, screenCaptureC, logger)).Run(ctx))
}()
client, server := buildClientServer(configService, dockerClient, screen, screenCaptureC, logger)
ch := runClientServer(ctx, t, client, server)
require.EventuallyWithT(
t,
@ -584,7 +550,9 @@ func TestIntegrationRestartDestination(t *testing.T) {
cancel()
<-done
result := <-ch
assert.ErrorContains(t, result.errClient, "context canceled")
assert.ErrorIs(t, result.errServer, context.Canceled)
}
func TestIntegrationStartDestinationFailed(t *testing.T) {
@ -602,14 +570,8 @@ func TestIntegrationStartDestinationFailed(t *testing.T) {
Destinations: []config.Destination{{Name: "Example server", URL: "rtmp://rtmp.example.com/live"}},
})
done := make(chan struct{})
go func() {
defer func() {
done <- struct{}{}
}()
require.Equal(t, context.Canceled, app.New(buildAppParams(t, configService, dockerClient, screen, screenCaptureC, logger)).Run(ctx))
}()
client, server := buildClientServer(configService, dockerClient, screen, screenCaptureC, logger)
ch := runClientServer(ctx, t, client, server)
require.EventuallyWithT(
t,
@ -658,7 +620,9 @@ func TestIntegrationStartDestinationFailed(t *testing.T) {
cancel()
<-done
result := <-ch
assert.ErrorContains(t, result.errClient, "context canceled")
assert.ErrorIs(t, result.errServer, context.Canceled)
}
func TestIntegrationDestinationValidations(t *testing.T) {
@ -675,14 +639,8 @@ func TestIntegrationDestinationValidations(t *testing.T) {
Sources: config.Sources{MediaServer: config.MediaServerSource{StreamKey: "live", RTMP: config.RTMPSource{Enabled: true}}},
})
done := make(chan struct{})
go func() {
defer func() {
done <- struct{}{}
}()
require.Equal(t, context.Canceled, app.New(buildAppParams(t, configService, dockerClient, screen, screenCaptureC, logger)).Run(ctx))
}()
client, server := buildClientServer(configService, dockerClient, screen, screenCaptureC, logger)
ch := runClientServer(ctx, t, client, server)
require.EventuallyWithT(
t,
@ -786,7 +744,9 @@ func TestIntegrationDestinationValidations(t *testing.T) {
printScreen(t, getContents, "After entering a duplicate destination URL")
cancel()
<-done
result := <-ch
assert.ErrorContains(t, result.errClient, "context canceled")
assert.ErrorIs(t, result.errServer, context.Canceled)
}
func TestIntegrationStartupCheck(t *testing.T) {
@ -817,14 +777,8 @@ func TestIntegrationStartupCheck(t *testing.T) {
configService := setupConfigService(t, config.Config{Sources: config.Sources{MediaServer: config.MediaServerSource{RTMP: config.RTMPSource{Enabled: true}}}})
screen, screenCaptureC, getContents := setupSimulationScreen(t)
done := make(chan struct{})
go func() {
defer func() {
done <- struct{}{}
}()
require.Equal(t, context.Canceled, app.New(buildAppParams(t, configService, dockerClient, screen, screenCaptureC, logger)).Run(ctx))
}()
client, server := buildClientServer(configService, dockerClient, screen, screenCaptureC, logger)
ch := runClientServer(ctx, t, client, server)
require.EventuallyWithT(
t,
@ -868,7 +822,9 @@ func TestIntegrationStartupCheck(t *testing.T) {
printScreen(t, getContents, "After starting the mediaserver")
cancel()
<-done
result := <-ch
assert.ErrorContains(t, result.errClient, "context canceled")
assert.ErrorIs(t, result.errServer, context.Canceled)
}
func TestIntegrationMediaServerError(t *testing.T) {
@ -886,18 +842,8 @@ func TestIntegrationMediaServerError(t *testing.T) {
configService := setupConfigService(t, config.Config{Sources: config.Sources{MediaServer: config.MediaServerSource{RTMP: config.RTMPSource{Enabled: true}}}})
screen, screenCaptureC, getContents := setupSimulationScreen(t)
done := make(chan struct{})
go func() {
defer func() {
done <- struct{}{}
}()
require.EqualError(
t,
app.New(buildAppParams(t, configService, dockerClient, screen, screenCaptureC, logger)).Run(ctx),
"media server exited",
)
}()
client, server := buildClientServer(configService, dockerClient, screen, screenCaptureC, logger)
ch := runClientServer(ctx, t, client, server)
require.EventuallyWithT(
t,
@ -911,10 +857,12 @@ func TestIntegrationMediaServerError(t *testing.T) {
)
printScreen(t, getContents, "Ater displaying the media server error modal")
// Quit the app, this should cause the done channel to receive.
// Quit the app:
sendKey(t, screen, tcell.KeyEnter, ' ')
<-done
result := <-ch
assert.ErrorContains(t, result.errClient, "context canceled")
assert.ErrorContains(t, result.errServer, "media server exited")
}
func TestIntegrationDockerClientError(t *testing.T) {
@ -929,18 +877,8 @@ func TestIntegrationDockerClientError(t *testing.T) {
configService := setupConfigService(t, config.Config{Sources: config.Sources{MediaServer: config.MediaServerSource{RTMP: config.RTMPSource{Enabled: true}}}})
screen, screenCaptureC, getContents := setupSimulationScreen(t)
done := make(chan struct{})
go func() {
defer func() {
done <- struct{}{}
}()
require.EqualError(
t,
app.New(buildAppParams(t, configService, &dockerClient, screen, screenCaptureC, logger)).Run(ctx),
"create container client: network create: boom",
)
}()
client, server := buildClientServer(configService, &dockerClient, screen, screenCaptureC, logger)
ch := runClientServer(ctx, t, client, server)
require.EventuallyWithT(
t,
@ -954,10 +892,12 @@ func TestIntegrationDockerClientError(t *testing.T) {
)
printScreen(t, getContents, "Ater displaying the fatal error modal")
// Quit the app, this should cause the done channel to receive.
// Quit the app:
sendKey(t, screen, tcell.KeyEnter, ' ')
<-done
result := <-ch
assert.ErrorContains(t, result.errClient, "context canceled")
assert.EqualError(t, result.errServer, "create container client: network create: boom")
}
func TestIntegrationDockerConnectionError(t *testing.T) {
@ -971,16 +911,8 @@ func TestIntegrationDockerConnectionError(t *testing.T) {
configService := setupConfigService(t, config.Config{Sources: config.Sources{MediaServer: config.MediaServerSource{RTMP: config.RTMPSource{Enabled: true}}}})
screen, screenCaptureC, getContents := setupSimulationScreen(t)
done := make(chan struct{})
go func() {
defer func() {
done <- struct{}{}
}()
err := app.New(buildAppParams(t, configService, dockerClient, screen, screenCaptureC, logger)).Run(ctx)
require.ErrorContains(t, err, "dial tcp: lookup docker.example.com")
require.ErrorContains(t, err, "no such host")
}()
client, server := buildClientServer(configService, dockerClient, screen, screenCaptureC, logger)
ch := runClientServer(ctx, t, client, server)
require.EventuallyWithT(
t,
@ -994,10 +926,13 @@ func TestIntegrationDockerConnectionError(t *testing.T) {
)
printScreen(t, getContents, "Ater displaying the fatal error modal")
// Quit the app, this should cause the done channel to receive.
// Quit the app:
sendKey(t, screen, tcell.KeyEnter, ' ')
<-done
result := <-ch
assert.ErrorContains(t, result.errClient, "context canceled")
assert.ErrorContains(t, result.errServer, "dial tcp: lookup docker.example.com")
assert.ErrorContains(t, result.errServer, "no such host")
}
func TestIntegrationCopyURLs(t *testing.T) {
@ -1067,14 +1002,8 @@ func TestIntegrationCopyURLs(t *testing.T) {
configService := setupConfigService(t, config.Config{Sources: config.Sources{MediaServer: tc.mediaServerConfig}})
screen, screenCaptureC, getContents := setupSimulationScreen(t)
done := make(chan struct{})
go func() {
defer func() {
done <- struct{}{}
}()
require.Equal(t, context.Canceled, app.New(buildAppParams(t, configService, dockerClient, screen, screenCaptureC, logger)).Run(ctx))
}()
client, server := buildClientServer(configService, dockerClient, screen, screenCaptureC, logger)
ch := runClientServer(ctx, t, client, server)
time.Sleep(3 * time.Second)
printScreen(t, getContents, "Ater loading the app")
@ -1095,7 +1024,9 @@ func TestIntegrationCopyURLs(t *testing.T) {
cancel()
<-done
result := <-ch
assert.ErrorContains(t, result.errClient, "context canceled")
assert.ErrorIs(t, result.errServer, context.Canceled)
})
}
}

View File

@ -2,6 +2,7 @@ package event
import (
"log/slog"
"slices"
"sync"
)
@ -31,6 +32,21 @@ func (b *Bus) Register() <-chan Event {
return ch
}
// Deregister deregisters a consumer for all events.
func (b *Bus) Deregister(ch <-chan Event) {
b.mu.Lock()
defer b.mu.Unlock()
b.consumers = slices.DeleteFunc(b.consumers, func(other chan Event) bool {
if ch == other {
close(other)
return true
}
return false
})
}
// Send sends an event to all registered consumers.
func (b *Bus) Send(evt Event) {
// The mutex is needed to ensure the backing array of b.consumers cannot be
@ -43,7 +59,7 @@ func (b *Bus) Send(evt Event) {
select {
case ch <- evt:
default:
b.logger.Warn("Event dropped", "name", evt.name())
b.logger.Warn("Event dropped", "name", evt.EventName())
}
}
}

View File

@ -6,6 +6,7 @@ import (
"git.netflux.io/rob/octoplex/internal/event"
"git.netflux.io/rob/octoplex/internal/testhelpers"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestBus(t *testing.T) {
@ -25,5 +26,19 @@ func TestBus(t *testing.T) {
}()
assert.Equal(t, evt, (<-ch1).(event.MediaServerStartedEvent))
assert.Equal(t, evt, (<-ch1).(event.MediaServerStartedEvent))
assert.Equal(t, evt, (<-ch2).(event.MediaServerStartedEvent))
assert.Equal(t, evt, (<-ch2).(event.MediaServerStartedEvent))
bus.Deregister(ch1)
_, ok := <-ch1
assert.False(t, ok)
select {
case <-ch2:
require.Fail(t, "ch2 should be blocking")
default:
}
}

View File

@ -49,12 +49,12 @@ func (c CommandCloseOtherInstance) Name() string {
return "close_other_instance"
}
// CommandQuit quits the app.
type CommandQuit struct{}
// CommandKillServer kills the server.
type CommandKillServer struct{}
// Name implements the Command interface.
func (c CommandQuit) Name() string {
return "quit"
func (c CommandKillServer) Name() string {
return "kill_server"
}
// Command is an interface for commands that can be triggered by the terminal

View File

@ -19,7 +19,7 @@ const (
// Event represents something which happened in the appllication.
type Event interface {
name() Name
EventName() Name
}
// AppStateChangedEvent is emitted when the application state changes.
@ -27,7 +27,7 @@ type AppStateChangedEvent struct {
State domain.AppState
}
func (e AppStateChangedEvent) name() Name {
func (e AppStateChangedEvent) EventName() Name {
return EventNameAppStateChanged
}
@ -36,16 +36,17 @@ type DestinationAddedEvent struct {
URL string
}
func (e DestinationAddedEvent) name() Name {
func (e DestinationAddedEvent) EventName() Name {
return EventNameDestinationAdded
}
// AddDestinationFailedEvent is emitted when a destination fails to be added.
type AddDestinationFailedEvent struct {
URL string
Err error
}
func (e AddDestinationFailedEvent) name() Name {
func (e AddDestinationFailedEvent) EventName() Name {
return EventNameAddDestinationFailed
}
@ -55,14 +56,17 @@ type DestinationStreamExitedEvent struct {
Err error
}
func (e DestinationStreamExitedEvent) name() Name {
func (e DestinationStreamExitedEvent) EventName() Name {
return EventNameDestinationStreamExited
}
// StartDestinationFailedEvent is emitted when a destination fails to start.
type StartDestinationFailedEvent struct{}
type StartDestinationFailedEvent struct {
URL string
Message string
}
func (e StartDestinationFailedEvent) name() Name {
func (e StartDestinationFailedEvent) EventName() Name {
return EventNameStartDestinationFailed
}
@ -72,17 +76,18 @@ type DestinationRemovedEvent struct {
URL string
}
func (e DestinationRemovedEvent) name() Name {
func (e DestinationRemovedEvent) EventName() Name {
return EventNameDestinationRemoved
}
// RemoveDestinationFailedEvent is emitted when a destination fails to be
// removed.
type RemoveDestinationFailedEvent struct {
URL string
Err error
}
func (e RemoveDestinationFailedEvent) name() Name {
func (e RemoveDestinationFailedEvent) EventName() Name {
return EventNameRemoveDestinationFailed
}
@ -95,11 +100,11 @@ type FatalErrorOccurredEvent struct {
// OtherInstanceDetectedEvent is emitted when the app launches and detects another instance.
type OtherInstanceDetectedEvent struct{}
func (e OtherInstanceDetectedEvent) name() Name {
func (e OtherInstanceDetectedEvent) EventName() Name {
return EventNameOtherInstanceDetected
}
func (e FatalErrorOccurredEvent) name() Name {
func (e FatalErrorOccurredEvent) EventName() Name {
return "fatal_error_occurred"
}
@ -109,6 +114,6 @@ type MediaServerStartedEvent struct {
RTMPSURL string
}
func (e MediaServerStartedEvent) name() Name {
func (e MediaServerStartedEvent) EventName() Name {
return "media_server_started"
}

View File

View File

@ -0,0 +1,199 @@
// Code generated by protoc-gen-go. DO NOT EDIT.
// versions:
// protoc-gen-go v1.28.1
// protoc v6.30.1
// source: api.proto
package grpc
import (
protoreflect "google.golang.org/protobuf/reflect/protoreflect"
protoimpl "google.golang.org/protobuf/runtime/protoimpl"
reflect "reflect"
sync "sync"
)
const (
// Verify that this generated code is sufficiently up-to-date.
_ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion)
// Verify that runtime/protoimpl is sufficiently up-to-date.
_ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20)
)
type Envelope struct {
state protoimpl.MessageState
sizeCache protoimpl.SizeCache
unknownFields protoimpl.UnknownFields
// Types that are assignable to Payload:
//
// *Envelope_Command
// *Envelope_Event
Payload isEnvelope_Payload `protobuf_oneof:"payload"`
}
func (x *Envelope) Reset() {
*x = Envelope{}
if protoimpl.UnsafeEnabled {
mi := &file_api_proto_msgTypes[0]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
}
func (x *Envelope) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*Envelope) ProtoMessage() {}
func (x *Envelope) ProtoReflect() protoreflect.Message {
mi := &file_api_proto_msgTypes[0]
if protoimpl.UnsafeEnabled && x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use Envelope.ProtoReflect.Descriptor instead.
func (*Envelope) Descriptor() ([]byte, []int) {
return file_api_proto_rawDescGZIP(), []int{0}
}
func (m *Envelope) GetPayload() isEnvelope_Payload {
if m != nil {
return m.Payload
}
return nil
}
func (x *Envelope) GetCommand() *Command {
if x, ok := x.GetPayload().(*Envelope_Command); ok {
return x.Command
}
return nil
}
func (x *Envelope) GetEvent() *Event {
if x, ok := x.GetPayload().(*Envelope_Event); ok {
return x.Event
}
return nil
}
type isEnvelope_Payload interface {
isEnvelope_Payload()
}
type Envelope_Command struct {
Command *Command `protobuf:"bytes,1,opt,name=command,proto3,oneof"`
}
type Envelope_Event struct {
Event *Event `protobuf:"bytes,2,opt,name=event,proto3,oneof"`
}
func (*Envelope_Command) isEnvelope_Payload() {}
func (*Envelope_Event) isEnvelope_Payload() {}
var File_api_proto protoreflect.FileDescriptor
var file_api_proto_rawDesc = []byte{
0x0a, 0x09, 0x61, 0x70, 0x69, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x12, 0x03, 0x61, 0x70, 0x69,
0x1a, 0x0b, 0x65, 0x76, 0x65, 0x6e, 0x74, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x1a, 0x0d, 0x63,
0x6f, 0x6d, 0x6d, 0x61, 0x6e, 0x64, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x22, 0x63, 0x0a, 0x08,
0x45, 0x6e, 0x76, 0x65, 0x6c, 0x6f, 0x70, 0x65, 0x12, 0x28, 0x0a, 0x07, 0x63, 0x6f, 0x6d, 0x6d,
0x61, 0x6e, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x0c, 0x2e, 0x61, 0x70, 0x69, 0x2e,
0x43, 0x6f, 0x6d, 0x6d, 0x61, 0x6e, 0x64, 0x48, 0x00, 0x52, 0x07, 0x63, 0x6f, 0x6d, 0x6d, 0x61,
0x6e, 0x64, 0x12, 0x22, 0x0a, 0x05, 0x65, 0x76, 0x65, 0x6e, 0x74, 0x18, 0x02, 0x20, 0x01, 0x28,
0x0b, 0x32, 0x0a, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x45, 0x76, 0x65, 0x6e, 0x74, 0x48, 0x00, 0x52,
0x05, 0x65, 0x76, 0x65, 0x6e, 0x74, 0x42, 0x09, 0x0a, 0x07, 0x70, 0x61, 0x79, 0x6c, 0x6f, 0x61,
0x64, 0x32, 0x3e, 0x0a, 0x0b, 0x49, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x41, 0x50, 0x49,
0x12, 0x2f, 0x0a, 0x0b, 0x43, 0x6f, 0x6d, 0x6d, 0x75, 0x6e, 0x69, 0x63, 0x61, 0x74, 0x65, 0x12,
0x0d, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x45, 0x6e, 0x76, 0x65, 0x6c, 0x6f, 0x70, 0x65, 0x1a, 0x0d,
0x2e, 0x61, 0x70, 0x69, 0x2e, 0x45, 0x6e, 0x76, 0x65, 0x6c, 0x6f, 0x70, 0x65, 0x28, 0x01, 0x30,
0x01, 0x42, 0x35, 0x5a, 0x33, 0x67, 0x69, 0x74, 0x2e, 0x6e, 0x65, 0x74, 0x66, 0x6c, 0x75, 0x78,
0x2e, 0x69, 0x6f, 0x2f, 0x72, 0x6f, 0x62, 0x2f, 0x6f, 0x63, 0x74, 0x6f, 0x70, 0x6c, 0x65, 0x78,
0x2f, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x2f, 0x67, 0x65, 0x6e, 0x65, 0x72, 0x61,
0x74, 0x65, 0x64, 0x2f, 0x67, 0x72, 0x70, 0x63, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33,
}
var (
file_api_proto_rawDescOnce sync.Once
file_api_proto_rawDescData = file_api_proto_rawDesc
)
func file_api_proto_rawDescGZIP() []byte {
file_api_proto_rawDescOnce.Do(func() {
file_api_proto_rawDescData = protoimpl.X.CompressGZIP(file_api_proto_rawDescData)
})
return file_api_proto_rawDescData
}
var file_api_proto_msgTypes = make([]protoimpl.MessageInfo, 1)
var file_api_proto_goTypes = []interface{}{
(*Envelope)(nil), // 0: api.Envelope
(*Command)(nil), // 1: api.Command
(*Event)(nil), // 2: api.Event
}
var file_api_proto_depIdxs = []int32{
1, // 0: api.Envelope.command:type_name -> api.Command
2, // 1: api.Envelope.event:type_name -> api.Event
0, // 2: api.InternalAPI.Communicate:input_type -> api.Envelope
0, // 3: api.InternalAPI.Communicate:output_type -> api.Envelope
3, // [3:4] is the sub-list for method output_type
2, // [2:3] is the sub-list for method input_type
2, // [2:2] is the sub-list for extension type_name
2, // [2:2] is the sub-list for extension extendee
0, // [0:2] is the sub-list for field type_name
}
func init() { file_api_proto_init() }
func file_api_proto_init() {
if File_api_proto != nil {
return
}
file_event_proto_init()
file_command_proto_init()
if !protoimpl.UnsafeEnabled {
file_api_proto_msgTypes[0].Exporter = func(v interface{}, i int) interface{} {
switch v := v.(*Envelope); i {
case 0:
return &v.state
case 1:
return &v.sizeCache
case 2:
return &v.unknownFields
default:
return nil
}
}
}
file_api_proto_msgTypes[0].OneofWrappers = []interface{}{
(*Envelope_Command)(nil),
(*Envelope_Event)(nil),
}
type x struct{}
out := protoimpl.TypeBuilder{
File: protoimpl.DescBuilder{
GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
RawDescriptor: file_api_proto_rawDesc,
NumEnums: 0,
NumMessages: 1,
NumExtensions: 0,
NumServices: 1,
},
GoTypes: file_api_proto_goTypes,
DependencyIndexes: file_api_proto_depIdxs,
MessageInfos: file_api_proto_msgTypes,
}.Build()
File_api_proto = out.File
file_api_proto_rawDesc = nil
file_api_proto_goTypes = nil
file_api_proto_depIdxs = nil
}

View File

@ -0,0 +1,137 @@
// Code generated by protoc-gen-go-grpc. DO NOT EDIT.
// versions:
// - protoc-gen-go-grpc v1.2.0
// - protoc v6.30.1
// source: api.proto
package grpc
import (
context "context"
grpc "google.golang.org/grpc"
codes "google.golang.org/grpc/codes"
status "google.golang.org/grpc/status"
)
// This is a compile-time assertion to ensure that this generated file
// is compatible with the grpc package it is being compiled against.
// Requires gRPC-Go v1.32.0 or later.
const _ = grpc.SupportPackageIsVersion7
// InternalAPIClient is the client API for InternalAPI service.
//
// For semantics around ctx use and closing/ending streaming RPCs, please refer to https://pkg.go.dev/google.golang.org/grpc/?tab=doc#ClientConn.NewStream.
type InternalAPIClient interface {
Communicate(ctx context.Context, opts ...grpc.CallOption) (InternalAPI_CommunicateClient, error)
}
type internalAPIClient struct {
cc grpc.ClientConnInterface
}
func NewInternalAPIClient(cc grpc.ClientConnInterface) InternalAPIClient {
return &internalAPIClient{cc}
}
func (c *internalAPIClient) Communicate(ctx context.Context, opts ...grpc.CallOption) (InternalAPI_CommunicateClient, error) {
stream, err := c.cc.NewStream(ctx, &InternalAPI_ServiceDesc.Streams[0], "/api.InternalAPI/Communicate", opts...)
if err != nil {
return nil, err
}
x := &internalAPICommunicateClient{stream}
return x, nil
}
type InternalAPI_CommunicateClient interface {
Send(*Envelope) error
Recv() (*Envelope, error)
grpc.ClientStream
}
type internalAPICommunicateClient struct {
grpc.ClientStream
}
func (x *internalAPICommunicateClient) Send(m *Envelope) error {
return x.ClientStream.SendMsg(m)
}
func (x *internalAPICommunicateClient) Recv() (*Envelope, error) {
m := new(Envelope)
if err := x.ClientStream.RecvMsg(m); err != nil {
return nil, err
}
return m, nil
}
// InternalAPIServer is the server API for InternalAPI service.
// All implementations must embed UnimplementedInternalAPIServer
// for forward compatibility
type InternalAPIServer interface {
Communicate(InternalAPI_CommunicateServer) error
mustEmbedUnimplementedInternalAPIServer()
}
// UnimplementedInternalAPIServer must be embedded to have forward compatible implementations.
type UnimplementedInternalAPIServer struct {
}
func (UnimplementedInternalAPIServer) Communicate(InternalAPI_CommunicateServer) error {
return status.Errorf(codes.Unimplemented, "method Communicate not implemented")
}
func (UnimplementedInternalAPIServer) mustEmbedUnimplementedInternalAPIServer() {}
// UnsafeInternalAPIServer may be embedded to opt out of forward compatibility for this service.
// Use of this interface is not recommended, as added methods to InternalAPIServer will
// result in compilation errors.
type UnsafeInternalAPIServer interface {
mustEmbedUnimplementedInternalAPIServer()
}
func RegisterInternalAPIServer(s grpc.ServiceRegistrar, srv InternalAPIServer) {
s.RegisterService(&InternalAPI_ServiceDesc, srv)
}
func _InternalAPI_Communicate_Handler(srv interface{}, stream grpc.ServerStream) error {
return srv.(InternalAPIServer).Communicate(&internalAPICommunicateServer{stream})
}
type InternalAPI_CommunicateServer interface {
Send(*Envelope) error
Recv() (*Envelope, error)
grpc.ServerStream
}
type internalAPICommunicateServer struct {
grpc.ServerStream
}
func (x *internalAPICommunicateServer) Send(m *Envelope) error {
return x.ServerStream.SendMsg(m)
}
func (x *internalAPICommunicateServer) Recv() (*Envelope, error) {
m := new(Envelope)
if err := x.ServerStream.RecvMsg(m); err != nil {
return nil, err
}
return m, nil
}
// InternalAPI_ServiceDesc is the grpc.ServiceDesc for InternalAPI service.
// It's only intended for direct use with grpc.RegisterService,
// and not to be introspected or modified (even as a copy)
var InternalAPI_ServiceDesc = grpc.ServiceDesc{
ServiceName: "api.InternalAPI",
HandlerType: (*InternalAPIServer)(nil),
Methods: []grpc.MethodDesc{},
Streams: []grpc.StreamDesc{
{
StreamName: "Communicate",
Handler: _InternalAPI_Communicate_Handler,
ServerStreams: true,
ClientStreams: true,
},
},
Metadata: "api.proto",
}

View File

@ -0,0 +1,714 @@
// Code generated by protoc-gen-go. DO NOT EDIT.
// versions:
// protoc-gen-go v1.28.1
// protoc v6.30.1
// source: command.proto
package grpc
import (
protoreflect "google.golang.org/protobuf/reflect/protoreflect"
protoimpl "google.golang.org/protobuf/runtime/protoimpl"
reflect "reflect"
sync "sync"
)
const (
// Verify that this generated code is sufficiently up-to-date.
_ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion)
// Verify that runtime/protoimpl is sufficiently up-to-date.
_ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20)
)
type Command struct {
state protoimpl.MessageState
sizeCache protoimpl.SizeCache
unknownFields protoimpl.UnknownFields
// Types that are assignable to CommandType:
//
// *Command_AddDestination
// *Command_RemoveDestination
// *Command_StartDestination
// *Command_StopDestination
// *Command_CloseOtherInstances
// *Command_KillServer
// *Command_StartHandshake
CommandType isCommand_CommandType `protobuf_oneof:"command_type"`
}
func (x *Command) Reset() {
*x = Command{}
if protoimpl.UnsafeEnabled {
mi := &file_command_proto_msgTypes[0]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
}
func (x *Command) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*Command) ProtoMessage() {}
func (x *Command) ProtoReflect() protoreflect.Message {
mi := &file_command_proto_msgTypes[0]
if protoimpl.UnsafeEnabled && x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use Command.ProtoReflect.Descriptor instead.
func (*Command) Descriptor() ([]byte, []int) {
return file_command_proto_rawDescGZIP(), []int{0}
}
func (m *Command) GetCommandType() isCommand_CommandType {
if m != nil {
return m.CommandType
}
return nil
}
func (x *Command) GetAddDestination() *AddDestinationCommand {
if x, ok := x.GetCommandType().(*Command_AddDestination); ok {
return x.AddDestination
}
return nil
}
func (x *Command) GetRemoveDestination() *RemoveDestinationCommand {
if x, ok := x.GetCommandType().(*Command_RemoveDestination); ok {
return x.RemoveDestination
}
return nil
}
func (x *Command) GetStartDestination() *StartDestinationCommand {
if x, ok := x.GetCommandType().(*Command_StartDestination); ok {
return x.StartDestination
}
return nil
}
func (x *Command) GetStopDestination() *StopDestinationCommand {
if x, ok := x.GetCommandType().(*Command_StopDestination); ok {
return x.StopDestination
}
return nil
}
func (x *Command) GetCloseOtherInstances() *CloseOtherInstancesCommand {
if x, ok := x.GetCommandType().(*Command_CloseOtherInstances); ok {
return x.CloseOtherInstances
}
return nil
}
func (x *Command) GetKillServer() *KillServerCommand {
if x, ok := x.GetCommandType().(*Command_KillServer); ok {
return x.KillServer
}
return nil
}
func (x *Command) GetStartHandshake() *StartHandshakeCommand {
if x, ok := x.GetCommandType().(*Command_StartHandshake); ok {
return x.StartHandshake
}
return nil
}
type isCommand_CommandType interface {
isCommand_CommandType()
}
type Command_AddDestination struct {
AddDestination *AddDestinationCommand `protobuf:"bytes,1,opt,name=add_destination,json=addDestination,proto3,oneof"`
}
type Command_RemoveDestination struct {
RemoveDestination *RemoveDestinationCommand `protobuf:"bytes,2,opt,name=remove_destination,json=removeDestination,proto3,oneof"`
}
type Command_StartDestination struct {
StartDestination *StartDestinationCommand `protobuf:"bytes,3,opt,name=start_destination,json=startDestination,proto3,oneof"`
}
type Command_StopDestination struct {
StopDestination *StopDestinationCommand `protobuf:"bytes,4,opt,name=stop_destination,json=stopDestination,proto3,oneof"`
}
type Command_CloseOtherInstances struct {
CloseOtherInstances *CloseOtherInstancesCommand `protobuf:"bytes,5,opt,name=close_other_instances,json=closeOtherInstances,proto3,oneof"`
}
type Command_KillServer struct {
KillServer *KillServerCommand `protobuf:"bytes,6,opt,name=kill_server,json=killServer,proto3,oneof"`
}
type Command_StartHandshake struct {
StartHandshake *StartHandshakeCommand `protobuf:"bytes,7,opt,name=start_handshake,json=startHandshake,proto3,oneof"`
}
func (*Command_AddDestination) isCommand_CommandType() {}
func (*Command_RemoveDestination) isCommand_CommandType() {}
func (*Command_StartDestination) isCommand_CommandType() {}
func (*Command_StopDestination) isCommand_CommandType() {}
func (*Command_CloseOtherInstances) isCommand_CommandType() {}
func (*Command_KillServer) isCommand_CommandType() {}
func (*Command_StartHandshake) isCommand_CommandType() {}
type AddDestinationCommand struct {
state protoimpl.MessageState
sizeCache protoimpl.SizeCache
unknownFields protoimpl.UnknownFields
Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"`
Url string `protobuf:"bytes,2,opt,name=url,proto3" json:"url,omitempty"`
}
func (x *AddDestinationCommand) Reset() {
*x = AddDestinationCommand{}
if protoimpl.UnsafeEnabled {
mi := &file_command_proto_msgTypes[1]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
}
func (x *AddDestinationCommand) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*AddDestinationCommand) ProtoMessage() {}
func (x *AddDestinationCommand) ProtoReflect() protoreflect.Message {
mi := &file_command_proto_msgTypes[1]
if protoimpl.UnsafeEnabled && x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use AddDestinationCommand.ProtoReflect.Descriptor instead.
func (*AddDestinationCommand) Descriptor() ([]byte, []int) {
return file_command_proto_rawDescGZIP(), []int{1}
}
func (x *AddDestinationCommand) GetName() string {
if x != nil {
return x.Name
}
return ""
}
func (x *AddDestinationCommand) GetUrl() string {
if x != nil {
return x.Url
}
return ""
}
type RemoveDestinationCommand struct {
state protoimpl.MessageState
sizeCache protoimpl.SizeCache
unknownFields protoimpl.UnknownFields
Url string `protobuf:"bytes,1,opt,name=url,proto3" json:"url,omitempty"`
}
func (x *RemoveDestinationCommand) Reset() {
*x = RemoveDestinationCommand{}
if protoimpl.UnsafeEnabled {
mi := &file_command_proto_msgTypes[2]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
}
func (x *RemoveDestinationCommand) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*RemoveDestinationCommand) ProtoMessage() {}
func (x *RemoveDestinationCommand) ProtoReflect() protoreflect.Message {
mi := &file_command_proto_msgTypes[2]
if protoimpl.UnsafeEnabled && x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use RemoveDestinationCommand.ProtoReflect.Descriptor instead.
func (*RemoveDestinationCommand) Descriptor() ([]byte, []int) {
return file_command_proto_rawDescGZIP(), []int{2}
}
func (x *RemoveDestinationCommand) GetUrl() string {
if x != nil {
return x.Url
}
return ""
}
type StartDestinationCommand struct {
state protoimpl.MessageState
sizeCache protoimpl.SizeCache
unknownFields protoimpl.UnknownFields
Url string `protobuf:"bytes,1,opt,name=url,proto3" json:"url,omitempty"`
}
func (x *StartDestinationCommand) Reset() {
*x = StartDestinationCommand{}
if protoimpl.UnsafeEnabled {
mi := &file_command_proto_msgTypes[3]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
}
func (x *StartDestinationCommand) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*StartDestinationCommand) ProtoMessage() {}
func (x *StartDestinationCommand) ProtoReflect() protoreflect.Message {
mi := &file_command_proto_msgTypes[3]
if protoimpl.UnsafeEnabled && x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use StartDestinationCommand.ProtoReflect.Descriptor instead.
func (*StartDestinationCommand) Descriptor() ([]byte, []int) {
return file_command_proto_rawDescGZIP(), []int{3}
}
func (x *StartDestinationCommand) GetUrl() string {
if x != nil {
return x.Url
}
return ""
}
type StopDestinationCommand struct {
state protoimpl.MessageState
sizeCache protoimpl.SizeCache
unknownFields protoimpl.UnknownFields
Url string `protobuf:"bytes,1,opt,name=url,proto3" json:"url,omitempty"`
}
func (x *StopDestinationCommand) Reset() {
*x = StopDestinationCommand{}
if protoimpl.UnsafeEnabled {
mi := &file_command_proto_msgTypes[4]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
}
func (x *StopDestinationCommand) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*StopDestinationCommand) ProtoMessage() {}
func (x *StopDestinationCommand) ProtoReflect() protoreflect.Message {
mi := &file_command_proto_msgTypes[4]
if protoimpl.UnsafeEnabled && x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use StopDestinationCommand.ProtoReflect.Descriptor instead.
func (*StopDestinationCommand) Descriptor() ([]byte, []int) {
return file_command_proto_rawDescGZIP(), []int{4}
}
func (x *StopDestinationCommand) GetUrl() string {
if x != nil {
return x.Url
}
return ""
}
type CloseOtherInstancesCommand struct {
state protoimpl.MessageState
sizeCache protoimpl.SizeCache
unknownFields protoimpl.UnknownFields
}
func (x *CloseOtherInstancesCommand) Reset() {
*x = CloseOtherInstancesCommand{}
if protoimpl.UnsafeEnabled {
mi := &file_command_proto_msgTypes[5]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
}
func (x *CloseOtherInstancesCommand) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*CloseOtherInstancesCommand) ProtoMessage() {}
func (x *CloseOtherInstancesCommand) ProtoReflect() protoreflect.Message {
mi := &file_command_proto_msgTypes[5]
if protoimpl.UnsafeEnabled && x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use CloseOtherInstancesCommand.ProtoReflect.Descriptor instead.
func (*CloseOtherInstancesCommand) Descriptor() ([]byte, []int) {
return file_command_proto_rawDescGZIP(), []int{5}
}
type KillServerCommand struct {
state protoimpl.MessageState
sizeCache protoimpl.SizeCache
unknownFields protoimpl.UnknownFields
}
func (x *KillServerCommand) Reset() {
*x = KillServerCommand{}
if protoimpl.UnsafeEnabled {
mi := &file_command_proto_msgTypes[6]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
}
func (x *KillServerCommand) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*KillServerCommand) ProtoMessage() {}
func (x *KillServerCommand) ProtoReflect() protoreflect.Message {
mi := &file_command_proto_msgTypes[6]
if protoimpl.UnsafeEnabled && x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use KillServerCommand.ProtoReflect.Descriptor instead.
func (*KillServerCommand) Descriptor() ([]byte, []int) {
return file_command_proto_rawDescGZIP(), []int{6}
}
type StartHandshakeCommand struct {
state protoimpl.MessageState
sizeCache protoimpl.SizeCache
unknownFields protoimpl.UnknownFields
}
func (x *StartHandshakeCommand) Reset() {
*x = StartHandshakeCommand{}
if protoimpl.UnsafeEnabled {
mi := &file_command_proto_msgTypes[7]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
}
func (x *StartHandshakeCommand) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*StartHandshakeCommand) ProtoMessage() {}
func (x *StartHandshakeCommand) ProtoReflect() protoreflect.Message {
mi := &file_command_proto_msgTypes[7]
if protoimpl.UnsafeEnabled && x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use StartHandshakeCommand.ProtoReflect.Descriptor instead.
func (*StartHandshakeCommand) Descriptor() ([]byte, []int) {
return file_command_proto_rawDescGZIP(), []int{7}
}
var File_command_proto protoreflect.FileDescriptor
var file_command_proto_rawDesc = []byte{
0x0a, 0x0d, 0x63, 0x6f, 0x6d, 0x6d, 0x61, 0x6e, 0x64, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x12,
0x03, 0x61, 0x70, 0x69, 0x22, 0xa0, 0x04, 0x0a, 0x07, 0x43, 0x6f, 0x6d, 0x6d, 0x61, 0x6e, 0x64,
0x12, 0x45, 0x0a, 0x0f, 0x61, 0x64, 0x64, 0x5f, 0x64, 0x65, 0x73, 0x74, 0x69, 0x6e, 0x61, 0x74,
0x69, 0x6f, 0x6e, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x61, 0x70, 0x69, 0x2e,
0x41, 0x64, 0x64, 0x44, 0x65, 0x73, 0x74, 0x69, 0x6e, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x43, 0x6f,
0x6d, 0x6d, 0x61, 0x6e, 0x64, 0x48, 0x00, 0x52, 0x0e, 0x61, 0x64, 0x64, 0x44, 0x65, 0x73, 0x74,
0x69, 0x6e, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x4e, 0x0a, 0x12, 0x72, 0x65, 0x6d, 0x6f, 0x76,
0x65, 0x5f, 0x64, 0x65, 0x73, 0x74, 0x69, 0x6e, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x18, 0x02, 0x20,
0x01, 0x28, 0x0b, 0x32, 0x1d, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x52, 0x65, 0x6d, 0x6f, 0x76, 0x65,
0x44, 0x65, 0x73, 0x74, 0x69, 0x6e, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x43, 0x6f, 0x6d, 0x6d, 0x61,
0x6e, 0x64, 0x48, 0x00, 0x52, 0x11, 0x72, 0x65, 0x6d, 0x6f, 0x76, 0x65, 0x44, 0x65, 0x73, 0x74,
0x69, 0x6e, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x4b, 0x0a, 0x11, 0x73, 0x74, 0x61, 0x72, 0x74,
0x5f, 0x64, 0x65, 0x73, 0x74, 0x69, 0x6e, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x18, 0x03, 0x20, 0x01,
0x28, 0x0b, 0x32, 0x1c, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x53, 0x74, 0x61, 0x72, 0x74, 0x44, 0x65,
0x73, 0x74, 0x69, 0x6e, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x43, 0x6f, 0x6d, 0x6d, 0x61, 0x6e, 0x64,
0x48, 0x00, 0x52, 0x10, 0x73, 0x74, 0x61, 0x72, 0x74, 0x44, 0x65, 0x73, 0x74, 0x69, 0x6e, 0x61,
0x74, 0x69, 0x6f, 0x6e, 0x12, 0x48, 0x0a, 0x10, 0x73, 0x74, 0x6f, 0x70, 0x5f, 0x64, 0x65, 0x73,
0x74, 0x69, 0x6e, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x18, 0x04, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1b,
0x2e, 0x61, 0x70, 0x69, 0x2e, 0x53, 0x74, 0x6f, 0x70, 0x44, 0x65, 0x73, 0x74, 0x69, 0x6e, 0x61,
0x74, 0x69, 0x6f, 0x6e, 0x43, 0x6f, 0x6d, 0x6d, 0x61, 0x6e, 0x64, 0x48, 0x00, 0x52, 0x0f, 0x73,
0x74, 0x6f, 0x70, 0x44, 0x65, 0x73, 0x74, 0x69, 0x6e, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x55,
0x0a, 0x15, 0x63, 0x6c, 0x6f, 0x73, 0x65, 0x5f, 0x6f, 0x74, 0x68, 0x65, 0x72, 0x5f, 0x69, 0x6e,
0x73, 0x74, 0x61, 0x6e, 0x63, 0x65, 0x73, 0x18, 0x05, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1f, 0x2e,
0x61, 0x70, 0x69, 0x2e, 0x43, 0x6c, 0x6f, 0x73, 0x65, 0x4f, 0x74, 0x68, 0x65, 0x72, 0x49, 0x6e,
0x73, 0x74, 0x61, 0x6e, 0x63, 0x65, 0x73, 0x43, 0x6f, 0x6d, 0x6d, 0x61, 0x6e, 0x64, 0x48, 0x00,
0x52, 0x13, 0x63, 0x6c, 0x6f, 0x73, 0x65, 0x4f, 0x74, 0x68, 0x65, 0x72, 0x49, 0x6e, 0x73, 0x74,
0x61, 0x6e, 0x63, 0x65, 0x73, 0x12, 0x39, 0x0a, 0x0b, 0x6b, 0x69, 0x6c, 0x6c, 0x5f, 0x73, 0x65,
0x72, 0x76, 0x65, 0x72, 0x18, 0x06, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x16, 0x2e, 0x61, 0x70, 0x69,
0x2e, 0x4b, 0x69, 0x6c, 0x6c, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x43, 0x6f, 0x6d, 0x6d, 0x61,
0x6e, 0x64, 0x48, 0x00, 0x52, 0x0a, 0x6b, 0x69, 0x6c, 0x6c, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72,
0x12, 0x45, 0x0a, 0x0f, 0x73, 0x74, 0x61, 0x72, 0x74, 0x5f, 0x68, 0x61, 0x6e, 0x64, 0x73, 0x68,
0x61, 0x6b, 0x65, 0x18, 0x07, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x61, 0x70, 0x69, 0x2e,
0x53, 0x74, 0x61, 0x72, 0x74, 0x48, 0x61, 0x6e, 0x64, 0x73, 0x68, 0x61, 0x6b, 0x65, 0x43, 0x6f,
0x6d, 0x6d, 0x61, 0x6e, 0x64, 0x48, 0x00, 0x52, 0x0e, 0x73, 0x74, 0x61, 0x72, 0x74, 0x48, 0x61,
0x6e, 0x64, 0x73, 0x68, 0x61, 0x6b, 0x65, 0x42, 0x0e, 0x0a, 0x0c, 0x63, 0x6f, 0x6d, 0x6d, 0x61,
0x6e, 0x64, 0x5f, 0x74, 0x79, 0x70, 0x65, 0x22, 0x3d, 0x0a, 0x15, 0x41, 0x64, 0x64, 0x44, 0x65,
0x73, 0x74, 0x69, 0x6e, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x43, 0x6f, 0x6d, 0x6d, 0x61, 0x6e, 0x64,
0x12, 0x12, 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04,
0x6e, 0x61, 0x6d, 0x65, 0x12, 0x10, 0x0a, 0x03, 0x75, 0x72, 0x6c, 0x18, 0x02, 0x20, 0x01, 0x28,
0x09, 0x52, 0x03, 0x75, 0x72, 0x6c, 0x22, 0x2c, 0x0a, 0x18, 0x52, 0x65, 0x6d, 0x6f, 0x76, 0x65,
0x44, 0x65, 0x73, 0x74, 0x69, 0x6e, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x43, 0x6f, 0x6d, 0x6d, 0x61,
0x6e, 0x64, 0x12, 0x10, 0x0a, 0x03, 0x75, 0x72, 0x6c, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52,
0x03, 0x75, 0x72, 0x6c, 0x22, 0x2b, 0x0a, 0x17, 0x53, 0x74, 0x61, 0x72, 0x74, 0x44, 0x65, 0x73,
0x74, 0x69, 0x6e, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x43, 0x6f, 0x6d, 0x6d, 0x61, 0x6e, 0x64, 0x12,
0x10, 0x0a, 0x03, 0x75, 0x72, 0x6c, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x75, 0x72,
0x6c, 0x22, 0x2a, 0x0a, 0x16, 0x53, 0x74, 0x6f, 0x70, 0x44, 0x65, 0x73, 0x74, 0x69, 0x6e, 0x61,
0x74, 0x69, 0x6f, 0x6e, 0x43, 0x6f, 0x6d, 0x6d, 0x61, 0x6e, 0x64, 0x12, 0x10, 0x0a, 0x03, 0x75,
0x72, 0x6c, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x75, 0x72, 0x6c, 0x22, 0x1c, 0x0a,
0x1a, 0x43, 0x6c, 0x6f, 0x73, 0x65, 0x4f, 0x74, 0x68, 0x65, 0x72, 0x49, 0x6e, 0x73, 0x74, 0x61,
0x6e, 0x63, 0x65, 0x73, 0x43, 0x6f, 0x6d, 0x6d, 0x61, 0x6e, 0x64, 0x22, 0x13, 0x0a, 0x11, 0x4b,
0x69, 0x6c, 0x6c, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x43, 0x6f, 0x6d, 0x6d, 0x61, 0x6e, 0x64,
0x22, 0x17, 0x0a, 0x15, 0x53, 0x74, 0x61, 0x72, 0x74, 0x48, 0x61, 0x6e, 0x64, 0x73, 0x68, 0x61,
0x6b, 0x65, 0x43, 0x6f, 0x6d, 0x6d, 0x61, 0x6e, 0x64, 0x42, 0x35, 0x5a, 0x33, 0x67, 0x69, 0x74,
0x2e, 0x6e, 0x65, 0x74, 0x66, 0x6c, 0x75, 0x78, 0x2e, 0x69, 0x6f, 0x2f, 0x72, 0x6f, 0x62, 0x2f,
0x6f, 0x63, 0x74, 0x6f, 0x70, 0x6c, 0x65, 0x78, 0x2f, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x61,
0x6c, 0x2f, 0x67, 0x65, 0x6e, 0x65, 0x72, 0x61, 0x74, 0x65, 0x64, 0x2f, 0x67, 0x72, 0x70, 0x63,
0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33,
}
var (
file_command_proto_rawDescOnce sync.Once
file_command_proto_rawDescData = file_command_proto_rawDesc
)
func file_command_proto_rawDescGZIP() []byte {
file_command_proto_rawDescOnce.Do(func() {
file_command_proto_rawDescData = protoimpl.X.CompressGZIP(file_command_proto_rawDescData)
})
return file_command_proto_rawDescData
}
var file_command_proto_msgTypes = make([]protoimpl.MessageInfo, 8)
var file_command_proto_goTypes = []interface{}{
(*Command)(nil), // 0: api.Command
(*AddDestinationCommand)(nil), // 1: api.AddDestinationCommand
(*RemoveDestinationCommand)(nil), // 2: api.RemoveDestinationCommand
(*StartDestinationCommand)(nil), // 3: api.StartDestinationCommand
(*StopDestinationCommand)(nil), // 4: api.StopDestinationCommand
(*CloseOtherInstancesCommand)(nil), // 5: api.CloseOtherInstancesCommand
(*KillServerCommand)(nil), // 6: api.KillServerCommand
(*StartHandshakeCommand)(nil), // 7: api.StartHandshakeCommand
}
var file_command_proto_depIdxs = []int32{
1, // 0: api.Command.add_destination:type_name -> api.AddDestinationCommand
2, // 1: api.Command.remove_destination:type_name -> api.RemoveDestinationCommand
3, // 2: api.Command.start_destination:type_name -> api.StartDestinationCommand
4, // 3: api.Command.stop_destination:type_name -> api.StopDestinationCommand
5, // 4: api.Command.close_other_instances:type_name -> api.CloseOtherInstancesCommand
6, // 5: api.Command.kill_server:type_name -> api.KillServerCommand
7, // 6: api.Command.start_handshake:type_name -> api.StartHandshakeCommand
7, // [7:7] is the sub-list for method output_type
7, // [7:7] is the sub-list for method input_type
7, // [7:7] is the sub-list for extension type_name
7, // [7:7] is the sub-list for extension extendee
0, // [0:7] is the sub-list for field type_name
}
func init() { file_command_proto_init() }
func file_command_proto_init() {
if File_command_proto != nil {
return
}
if !protoimpl.UnsafeEnabled {
file_command_proto_msgTypes[0].Exporter = func(v interface{}, i int) interface{} {
switch v := v.(*Command); i {
case 0:
return &v.state
case 1:
return &v.sizeCache
case 2:
return &v.unknownFields
default:
return nil
}
}
file_command_proto_msgTypes[1].Exporter = func(v interface{}, i int) interface{} {
switch v := v.(*AddDestinationCommand); i {
case 0:
return &v.state
case 1:
return &v.sizeCache
case 2:
return &v.unknownFields
default:
return nil
}
}
file_command_proto_msgTypes[2].Exporter = func(v interface{}, i int) interface{} {
switch v := v.(*RemoveDestinationCommand); i {
case 0:
return &v.state
case 1:
return &v.sizeCache
case 2:
return &v.unknownFields
default:
return nil
}
}
file_command_proto_msgTypes[3].Exporter = func(v interface{}, i int) interface{} {
switch v := v.(*StartDestinationCommand); i {
case 0:
return &v.state
case 1:
return &v.sizeCache
case 2:
return &v.unknownFields
default:
return nil
}
}
file_command_proto_msgTypes[4].Exporter = func(v interface{}, i int) interface{} {
switch v := v.(*StopDestinationCommand); i {
case 0:
return &v.state
case 1:
return &v.sizeCache
case 2:
return &v.unknownFields
default:
return nil
}
}
file_command_proto_msgTypes[5].Exporter = func(v interface{}, i int) interface{} {
switch v := v.(*CloseOtherInstancesCommand); i {
case 0:
return &v.state
case 1:
return &v.sizeCache
case 2:
return &v.unknownFields
default:
return nil
}
}
file_command_proto_msgTypes[6].Exporter = func(v interface{}, i int) interface{} {
switch v := v.(*KillServerCommand); i {
case 0:
return &v.state
case 1:
return &v.sizeCache
case 2:
return &v.unknownFields
default:
return nil
}
}
file_command_proto_msgTypes[7].Exporter = func(v interface{}, i int) interface{} {
switch v := v.(*StartHandshakeCommand); i {
case 0:
return &v.state
case 1:
return &v.sizeCache
case 2:
return &v.unknownFields
default:
return nil
}
}
}
file_command_proto_msgTypes[0].OneofWrappers = []interface{}{
(*Command_AddDestination)(nil),
(*Command_RemoveDestination)(nil),
(*Command_StartDestination)(nil),
(*Command_StopDestination)(nil),
(*Command_CloseOtherInstances)(nil),
(*Command_KillServer)(nil),
(*Command_StartHandshake)(nil),
}
type x struct{}
out := protoimpl.TypeBuilder{
File: protoimpl.DescBuilder{
GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
RawDescriptor: file_command_proto_rawDesc,
NumEnums: 0,
NumMessages: 8,
NumExtensions: 0,
NumServices: 0,
},
GoTypes: file_command_proto_goTypes,
DependencyIndexes: file_command_proto_depIdxs,
MessageInfos: file_command_proto_msgTypes,
}.Build()
File_command_proto = out.File
file_command_proto_rawDesc = nil
file_command_proto_goTypes = nil
file_command_proto_depIdxs = nil
}

View File

@ -0,0 +1,729 @@
// Code generated by protoc-gen-go. DO NOT EDIT.
// versions:
// protoc-gen-go v1.28.1
// protoc v6.30.1
// source: domain.proto
package grpc
import (
protoreflect "google.golang.org/protobuf/reflect/protoreflect"
protoimpl "google.golang.org/protobuf/runtime/protoimpl"
timestamppb "google.golang.org/protobuf/types/known/timestamppb"
reflect "reflect"
sync "sync"
)
const (
// Verify that this generated code is sufficiently up-to-date.
_ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion)
// Verify that runtime/protoimpl is sufficiently up-to-date.
_ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20)
)
type Destination_Status int32
const (
Destination_STATUS_OFF_AIR Destination_Status = 0
Destination_STATUS_STARTING Destination_Status = 1
Destination_STATUS_LIVE Destination_Status = 2
)
// Enum value maps for Destination_Status.
var (
Destination_Status_name = map[int32]string{
0: "STATUS_OFF_AIR",
1: "STATUS_STARTING",
2: "STATUS_LIVE",
}
Destination_Status_value = map[string]int32{
"STATUS_OFF_AIR": 0,
"STATUS_STARTING": 1,
"STATUS_LIVE": 2,
}
)
func (x Destination_Status) Enum() *Destination_Status {
p := new(Destination_Status)
*p = x
return p
}
func (x Destination_Status) String() string {
return protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x))
}
func (Destination_Status) Descriptor() protoreflect.EnumDescriptor {
return file_domain_proto_enumTypes[0].Descriptor()
}
func (Destination_Status) Type() protoreflect.EnumType {
return &file_domain_proto_enumTypes[0]
}
func (x Destination_Status) Number() protoreflect.EnumNumber {
return protoreflect.EnumNumber(x)
}
// Deprecated: Use Destination_Status.Descriptor instead.
func (Destination_Status) EnumDescriptor() ([]byte, []int) {
return file_domain_proto_rawDescGZIP(), []int{2, 0}
}
type Container struct {
state protoimpl.MessageState
sizeCache protoimpl.SizeCache
unknownFields protoimpl.UnknownFields
Id string `protobuf:"bytes,1,opt,name=id,proto3" json:"id,omitempty"`
Status string `protobuf:"bytes,2,opt,name=status,proto3" json:"status,omitempty"`
HealthState string `protobuf:"bytes,3,opt,name=health_state,json=healthState,proto3" json:"health_state,omitempty"`
CpuPercent float64 `protobuf:"fixed64,4,opt,name=cpu_percent,json=cpuPercent,proto3" json:"cpu_percent,omitempty"`
MemoryUsageBytes uint64 `protobuf:"varint,5,opt,name=memory_usage_bytes,json=memoryUsageBytes,proto3" json:"memory_usage_bytes,omitempty"`
RxRate int32 `protobuf:"varint,6,opt,name=rx_rate,json=rxRate,proto3" json:"rx_rate,omitempty"`
TxRate int32 `protobuf:"varint,7,opt,name=tx_rate,json=txRate,proto3" json:"tx_rate,omitempty"`
RxSince *timestamppb.Timestamp `protobuf:"bytes,8,opt,name=rx_since,json=rxSince,proto3" json:"rx_since,omitempty"`
ImageName string `protobuf:"bytes,9,opt,name=image_name,json=imageName,proto3" json:"image_name,omitempty"`
PullStatus string `protobuf:"bytes,10,opt,name=pull_status,json=pullStatus,proto3" json:"pull_status,omitempty"`
PullProgress string `protobuf:"bytes,11,opt,name=pull_progress,json=pullProgress,proto3" json:"pull_progress,omitempty"`
PullPercent int32 `protobuf:"varint,12,opt,name=pull_percent,json=pullPercent,proto3" json:"pull_percent,omitempty"`
RestartCount int32 `protobuf:"varint,13,opt,name=restart_count,json=restartCount,proto3" json:"restart_count,omitempty"`
ExitCode *int32 `protobuf:"varint,14,opt,name=exit_code,json=exitCode,proto3,oneof" json:"exit_code,omitempty"`
Err string `protobuf:"bytes,15,opt,name=err,proto3" json:"err,omitempty"`
}
func (x *Container) Reset() {
*x = Container{}
if protoimpl.UnsafeEnabled {
mi := &file_domain_proto_msgTypes[0]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
}
func (x *Container) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*Container) ProtoMessage() {}
func (x *Container) ProtoReflect() protoreflect.Message {
mi := &file_domain_proto_msgTypes[0]
if protoimpl.UnsafeEnabled && x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use Container.ProtoReflect.Descriptor instead.
func (*Container) Descriptor() ([]byte, []int) {
return file_domain_proto_rawDescGZIP(), []int{0}
}
func (x *Container) GetId() string {
if x != nil {
return x.Id
}
return ""
}
func (x *Container) GetStatus() string {
if x != nil {
return x.Status
}
return ""
}
func (x *Container) GetHealthState() string {
if x != nil {
return x.HealthState
}
return ""
}
func (x *Container) GetCpuPercent() float64 {
if x != nil {
return x.CpuPercent
}
return 0
}
func (x *Container) GetMemoryUsageBytes() uint64 {
if x != nil {
return x.MemoryUsageBytes
}
return 0
}
func (x *Container) GetRxRate() int32 {
if x != nil {
return x.RxRate
}
return 0
}
func (x *Container) GetTxRate() int32 {
if x != nil {
return x.TxRate
}
return 0
}
func (x *Container) GetRxSince() *timestamppb.Timestamp {
if x != nil {
return x.RxSince
}
return nil
}
func (x *Container) GetImageName() string {
if x != nil {
return x.ImageName
}
return ""
}
func (x *Container) GetPullStatus() string {
if x != nil {
return x.PullStatus
}
return ""
}
func (x *Container) GetPullProgress() string {
if x != nil {
return x.PullProgress
}
return ""
}
func (x *Container) GetPullPercent() int32 {
if x != nil {
return x.PullPercent
}
return 0
}
func (x *Container) GetRestartCount() int32 {
if x != nil {
return x.RestartCount
}
return 0
}
func (x *Container) GetExitCode() int32 {
if x != nil && x.ExitCode != nil {
return *x.ExitCode
}
return 0
}
func (x *Container) GetErr() string {
if x != nil {
return x.Err
}
return ""
}
type Source struct {
state protoimpl.MessageState
sizeCache protoimpl.SizeCache
unknownFields protoimpl.UnknownFields
Container *Container `protobuf:"bytes,1,opt,name=container,proto3" json:"container,omitempty"`
Live bool `protobuf:"varint,2,opt,name=live,proto3" json:"live,omitempty"`
LiveChangedAt *timestamppb.Timestamp `protobuf:"bytes,3,opt,name=live_changed_at,json=liveChangedAt,proto3" json:"live_changed_at,omitempty"`
Tracks []string `protobuf:"bytes,4,rep,name=tracks,proto3" json:"tracks,omitempty"`
ExitReason string `protobuf:"bytes,5,opt,name=exit_reason,json=exitReason,proto3" json:"exit_reason,omitempty"`
}
func (x *Source) Reset() {
*x = Source{}
if protoimpl.UnsafeEnabled {
mi := &file_domain_proto_msgTypes[1]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
}
func (x *Source) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*Source) ProtoMessage() {}
func (x *Source) ProtoReflect() protoreflect.Message {
mi := &file_domain_proto_msgTypes[1]
if protoimpl.UnsafeEnabled && x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use Source.ProtoReflect.Descriptor instead.
func (*Source) Descriptor() ([]byte, []int) {
return file_domain_proto_rawDescGZIP(), []int{1}
}
func (x *Source) GetContainer() *Container {
if x != nil {
return x.Container
}
return nil
}
func (x *Source) GetLive() bool {
if x != nil {
return x.Live
}
return false
}
func (x *Source) GetLiveChangedAt() *timestamppb.Timestamp {
if x != nil {
return x.LiveChangedAt
}
return nil
}
func (x *Source) GetTracks() []string {
if x != nil {
return x.Tracks
}
return nil
}
func (x *Source) GetExitReason() string {
if x != nil {
return x.ExitReason
}
return ""
}
type Destination struct {
state protoimpl.MessageState
sizeCache protoimpl.SizeCache
unknownFields protoimpl.UnknownFields
Container *Container `protobuf:"bytes,1,opt,name=container,proto3" json:"container,omitempty"`
Status Destination_Status `protobuf:"varint,2,opt,name=status,proto3,enum=api.Destination_Status" json:"status,omitempty"`
Name string `protobuf:"bytes,3,opt,name=name,proto3" json:"name,omitempty"`
Url string `protobuf:"bytes,4,opt,name=url,proto3" json:"url,omitempty"`
}
func (x *Destination) Reset() {
*x = Destination{}
if protoimpl.UnsafeEnabled {
mi := &file_domain_proto_msgTypes[2]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
}
func (x *Destination) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*Destination) ProtoMessage() {}
func (x *Destination) ProtoReflect() protoreflect.Message {
mi := &file_domain_proto_msgTypes[2]
if protoimpl.UnsafeEnabled && x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use Destination.ProtoReflect.Descriptor instead.
func (*Destination) Descriptor() ([]byte, []int) {
return file_domain_proto_rawDescGZIP(), []int{2}
}
func (x *Destination) GetContainer() *Container {
if x != nil {
return x.Container
}
return nil
}
func (x *Destination) GetStatus() Destination_Status {
if x != nil {
return x.Status
}
return Destination_STATUS_OFF_AIR
}
func (x *Destination) GetName() string {
if x != nil {
return x.Name
}
return ""
}
func (x *Destination) GetUrl() string {
if x != nil {
return x.Url
}
return ""
}
type BuildInfo struct {
state protoimpl.MessageState
sizeCache protoimpl.SizeCache
unknownFields protoimpl.UnknownFields
GoVersion string `protobuf:"bytes,1,opt,name=go_version,json=goVersion,proto3" json:"go_version,omitempty"`
Version string `protobuf:"bytes,2,opt,name=version,proto3" json:"version,omitempty"`
Commit string `protobuf:"bytes,3,opt,name=commit,proto3" json:"commit,omitempty"`
Date string `protobuf:"bytes,4,opt,name=date,proto3" json:"date,omitempty"`
}
func (x *BuildInfo) Reset() {
*x = BuildInfo{}
if protoimpl.UnsafeEnabled {
mi := &file_domain_proto_msgTypes[3]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
}
func (x *BuildInfo) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*BuildInfo) ProtoMessage() {}
func (x *BuildInfo) ProtoReflect() protoreflect.Message {
mi := &file_domain_proto_msgTypes[3]
if protoimpl.UnsafeEnabled && x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use BuildInfo.ProtoReflect.Descriptor instead.
func (*BuildInfo) Descriptor() ([]byte, []int) {
return file_domain_proto_rawDescGZIP(), []int{3}
}
func (x *BuildInfo) GetGoVersion() string {
if x != nil {
return x.GoVersion
}
return ""
}
func (x *BuildInfo) GetVersion() string {
if x != nil {
return x.Version
}
return ""
}
func (x *BuildInfo) GetCommit() string {
if x != nil {
return x.Commit
}
return ""
}
func (x *BuildInfo) GetDate() string {
if x != nil {
return x.Date
}
return ""
}
type AppState struct {
state protoimpl.MessageState
sizeCache protoimpl.SizeCache
unknownFields protoimpl.UnknownFields
Source *Source `protobuf:"bytes,1,opt,name=source,proto3" json:"source,omitempty"`
Destinations []*Destination `protobuf:"bytes,2,rep,name=destinations,proto3" json:"destinations,omitempty"`
BuildInfo *BuildInfo `protobuf:"bytes,3,opt,name=build_info,json=buildInfo,proto3" json:"build_info,omitempty"`
}
func (x *AppState) Reset() {
*x = AppState{}
if protoimpl.UnsafeEnabled {
mi := &file_domain_proto_msgTypes[4]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
}
func (x *AppState) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*AppState) ProtoMessage() {}
func (x *AppState) ProtoReflect() protoreflect.Message {
mi := &file_domain_proto_msgTypes[4]
if protoimpl.UnsafeEnabled && x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use AppState.ProtoReflect.Descriptor instead.
func (*AppState) Descriptor() ([]byte, []int) {
return file_domain_proto_rawDescGZIP(), []int{4}
}
func (x *AppState) GetSource() *Source {
if x != nil {
return x.Source
}
return nil
}
func (x *AppState) GetDestinations() []*Destination {
if x != nil {
return x.Destinations
}
return nil
}
func (x *AppState) GetBuildInfo() *BuildInfo {
if x != nil {
return x.BuildInfo
}
return nil
}
var File_domain_proto protoreflect.FileDescriptor
var file_domain_proto_rawDesc = []byte{
0x0a, 0x0c, 0x64, 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x12, 0x03,
0x61, 0x70, 0x69, 0x1a, 0x1f, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2f, 0x70, 0x72, 0x6f, 0x74,
0x6f, 0x62, 0x75, 0x66, 0x2f, 0x74, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x2e, 0x70,
0x72, 0x6f, 0x74, 0x6f, 0x22, 0xfd, 0x03, 0x0a, 0x09, 0x43, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e,
0x65, 0x72, 0x12, 0x0e, 0x0a, 0x02, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x02,
0x69, 0x64, 0x12, 0x16, 0x0a, 0x06, 0x73, 0x74, 0x61, 0x74, 0x75, 0x73, 0x18, 0x02, 0x20, 0x01,
0x28, 0x09, 0x52, 0x06, 0x73, 0x74, 0x61, 0x74, 0x75, 0x73, 0x12, 0x21, 0x0a, 0x0c, 0x68, 0x65,
0x61, 0x6c, 0x74, 0x68, 0x5f, 0x73, 0x74, 0x61, 0x74, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09,
0x52, 0x0b, 0x68, 0x65, 0x61, 0x6c, 0x74, 0x68, 0x53, 0x74, 0x61, 0x74, 0x65, 0x12, 0x1f, 0x0a,
0x0b, 0x63, 0x70, 0x75, 0x5f, 0x70, 0x65, 0x72, 0x63, 0x65, 0x6e, 0x74, 0x18, 0x04, 0x20, 0x01,
0x28, 0x01, 0x52, 0x0a, 0x63, 0x70, 0x75, 0x50, 0x65, 0x72, 0x63, 0x65, 0x6e, 0x74, 0x12, 0x2c,
0x0a, 0x12, 0x6d, 0x65, 0x6d, 0x6f, 0x72, 0x79, 0x5f, 0x75, 0x73, 0x61, 0x67, 0x65, 0x5f, 0x62,
0x79, 0x74, 0x65, 0x73, 0x18, 0x05, 0x20, 0x01, 0x28, 0x04, 0x52, 0x10, 0x6d, 0x65, 0x6d, 0x6f,
0x72, 0x79, 0x55, 0x73, 0x61, 0x67, 0x65, 0x42, 0x79, 0x74, 0x65, 0x73, 0x12, 0x17, 0x0a, 0x07,
0x72, 0x78, 0x5f, 0x72, 0x61, 0x74, 0x65, 0x18, 0x06, 0x20, 0x01, 0x28, 0x05, 0x52, 0x06, 0x72,
0x78, 0x52, 0x61, 0x74, 0x65, 0x12, 0x17, 0x0a, 0x07, 0x74, 0x78, 0x5f, 0x72, 0x61, 0x74, 0x65,
0x18, 0x07, 0x20, 0x01, 0x28, 0x05, 0x52, 0x06, 0x74, 0x78, 0x52, 0x61, 0x74, 0x65, 0x12, 0x35,
0x0a, 0x08, 0x72, 0x78, 0x5f, 0x73, 0x69, 0x6e, 0x63, 0x65, 0x18, 0x08, 0x20, 0x01, 0x28, 0x0b,
0x32, 0x1a, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62,
0x75, 0x66, 0x2e, 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x52, 0x07, 0x72, 0x78,
0x53, 0x69, 0x6e, 0x63, 0x65, 0x12, 0x1d, 0x0a, 0x0a, 0x69, 0x6d, 0x61, 0x67, 0x65, 0x5f, 0x6e,
0x61, 0x6d, 0x65, 0x18, 0x09, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x69, 0x6d, 0x61, 0x67, 0x65,
0x4e, 0x61, 0x6d, 0x65, 0x12, 0x1f, 0x0a, 0x0b, 0x70, 0x75, 0x6c, 0x6c, 0x5f, 0x73, 0x74, 0x61,
0x74, 0x75, 0x73, 0x18, 0x0a, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0a, 0x70, 0x75, 0x6c, 0x6c, 0x53,
0x74, 0x61, 0x74, 0x75, 0x73, 0x12, 0x23, 0x0a, 0x0d, 0x70, 0x75, 0x6c, 0x6c, 0x5f, 0x70, 0x72,
0x6f, 0x67, 0x72, 0x65, 0x73, 0x73, 0x18, 0x0b, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0c, 0x70, 0x75,
0x6c, 0x6c, 0x50, 0x72, 0x6f, 0x67, 0x72, 0x65, 0x73, 0x73, 0x12, 0x21, 0x0a, 0x0c, 0x70, 0x75,
0x6c, 0x6c, 0x5f, 0x70, 0x65, 0x72, 0x63, 0x65, 0x6e, 0x74, 0x18, 0x0c, 0x20, 0x01, 0x28, 0x05,
0x52, 0x0b, 0x70, 0x75, 0x6c, 0x6c, 0x50, 0x65, 0x72, 0x63, 0x65, 0x6e, 0x74, 0x12, 0x23, 0x0a,
0x0d, 0x72, 0x65, 0x73, 0x74, 0x61, 0x72, 0x74, 0x5f, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x18, 0x0d,
0x20, 0x01, 0x28, 0x05, 0x52, 0x0c, 0x72, 0x65, 0x73, 0x74, 0x61, 0x72, 0x74, 0x43, 0x6f, 0x75,
0x6e, 0x74, 0x12, 0x20, 0x0a, 0x09, 0x65, 0x78, 0x69, 0x74, 0x5f, 0x63, 0x6f, 0x64, 0x65, 0x18,
0x0e, 0x20, 0x01, 0x28, 0x05, 0x48, 0x00, 0x52, 0x08, 0x65, 0x78, 0x69, 0x74, 0x43, 0x6f, 0x64,
0x65, 0x88, 0x01, 0x01, 0x12, 0x10, 0x0a, 0x03, 0x65, 0x72, 0x72, 0x18, 0x0f, 0x20, 0x01, 0x28,
0x09, 0x52, 0x03, 0x65, 0x72, 0x72, 0x42, 0x0c, 0x0a, 0x0a, 0x5f, 0x65, 0x78, 0x69, 0x74, 0x5f,
0x63, 0x6f, 0x64, 0x65, 0x22, 0xc7, 0x01, 0x0a, 0x06, 0x53, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x12,
0x2c, 0x0a, 0x09, 0x63, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x18, 0x01, 0x20, 0x01,
0x28, 0x0b, 0x32, 0x0e, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x43, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e,
0x65, 0x72, 0x52, 0x09, 0x63, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x12, 0x12, 0x0a,
0x04, 0x6c, 0x69, 0x76, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x08, 0x52, 0x04, 0x6c, 0x69, 0x76,
0x65, 0x12, 0x42, 0x0a, 0x0f, 0x6c, 0x69, 0x76, 0x65, 0x5f, 0x63, 0x68, 0x61, 0x6e, 0x67, 0x65,
0x64, 0x5f, 0x61, 0x74, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x67, 0x6f, 0x6f,
0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x54, 0x69, 0x6d,
0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x52, 0x0d, 0x6c, 0x69, 0x76, 0x65, 0x43, 0x68, 0x61, 0x6e,
0x67, 0x65, 0x64, 0x41, 0x74, 0x12, 0x16, 0x0a, 0x06, 0x74, 0x72, 0x61, 0x63, 0x6b, 0x73, 0x18,
0x04, 0x20, 0x03, 0x28, 0x09, 0x52, 0x06, 0x74, 0x72, 0x61, 0x63, 0x6b, 0x73, 0x12, 0x1f, 0x0a,
0x0b, 0x65, 0x78, 0x69, 0x74, 0x5f, 0x72, 0x65, 0x61, 0x73, 0x6f, 0x6e, 0x18, 0x05, 0x20, 0x01,
0x28, 0x09, 0x52, 0x0a, 0x65, 0x78, 0x69, 0x74, 0x52, 0x65, 0x61, 0x73, 0x6f, 0x6e, 0x22, 0xd6,
0x01, 0x0a, 0x0b, 0x44, 0x65, 0x73, 0x74, 0x69, 0x6e, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x2c,
0x0a, 0x09, 0x63, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x18, 0x01, 0x20, 0x01, 0x28,
0x0b, 0x32, 0x0e, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x43, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65,
0x72, 0x52, 0x09, 0x63, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x12, 0x2f, 0x0a, 0x06,
0x73, 0x74, 0x61, 0x74, 0x75, 0x73, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x17, 0x2e, 0x61,
0x70, 0x69, 0x2e, 0x44, 0x65, 0x73, 0x74, 0x69, 0x6e, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x2e, 0x53,
0x74, 0x61, 0x74, 0x75, 0x73, 0x52, 0x06, 0x73, 0x74, 0x61, 0x74, 0x75, 0x73, 0x12, 0x12, 0x0a,
0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x6e, 0x61, 0x6d,
0x65, 0x12, 0x10, 0x0a, 0x03, 0x75, 0x72, 0x6c, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03,
0x75, 0x72, 0x6c, 0x22, 0x42, 0x0a, 0x06, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x12, 0x12, 0x0a,
0x0e, 0x53, 0x54, 0x41, 0x54, 0x55, 0x53, 0x5f, 0x4f, 0x46, 0x46, 0x5f, 0x41, 0x49, 0x52, 0x10,
0x00, 0x12, 0x13, 0x0a, 0x0f, 0x53, 0x54, 0x41, 0x54, 0x55, 0x53, 0x5f, 0x53, 0x54, 0x41, 0x52,
0x54, 0x49, 0x4e, 0x47, 0x10, 0x01, 0x12, 0x0f, 0x0a, 0x0b, 0x53, 0x54, 0x41, 0x54, 0x55, 0x53,
0x5f, 0x4c, 0x49, 0x56, 0x45, 0x10, 0x02, 0x22, 0x70, 0x0a, 0x09, 0x42, 0x75, 0x69, 0x6c, 0x64,
0x49, 0x6e, 0x66, 0x6f, 0x12, 0x1d, 0x0a, 0x0a, 0x67, 0x6f, 0x5f, 0x76, 0x65, 0x72, 0x73, 0x69,
0x6f, 0x6e, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x67, 0x6f, 0x56, 0x65, 0x72, 0x73,
0x69, 0x6f, 0x6e, 0x12, 0x18, 0x0a, 0x07, 0x76, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x18, 0x02,
0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x76, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x12, 0x16, 0x0a,
0x06, 0x63, 0x6f, 0x6d, 0x6d, 0x69, 0x74, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x63,
0x6f, 0x6d, 0x6d, 0x69, 0x74, 0x12, 0x12, 0x0a, 0x04, 0x64, 0x61, 0x74, 0x65, 0x18, 0x04, 0x20,
0x01, 0x28, 0x09, 0x52, 0x04, 0x64, 0x61, 0x74, 0x65, 0x22, 0x94, 0x01, 0x0a, 0x08, 0x41, 0x70,
0x70, 0x53, 0x74, 0x61, 0x74, 0x65, 0x12, 0x23, 0x0a, 0x06, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65,
0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x0b, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x53, 0x6f, 0x75,
0x72, 0x63, 0x65, 0x52, 0x06, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x12, 0x34, 0x0a, 0x0c, 0x64,
0x65, 0x73, 0x74, 0x69, 0x6e, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x18, 0x02, 0x20, 0x03, 0x28,
0x0b, 0x32, 0x10, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x44, 0x65, 0x73, 0x74, 0x69, 0x6e, 0x61, 0x74,
0x69, 0x6f, 0x6e, 0x52, 0x0c, 0x64, 0x65, 0x73, 0x74, 0x69, 0x6e, 0x61, 0x74, 0x69, 0x6f, 0x6e,
0x73, 0x12, 0x2d, 0x0a, 0x0a, 0x62, 0x75, 0x69, 0x6c, 0x64, 0x5f, 0x69, 0x6e, 0x66, 0x6f, 0x18,
0x03, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x0e, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x42, 0x75, 0x69, 0x6c,
0x64, 0x49, 0x6e, 0x66, 0x6f, 0x52, 0x09, 0x62, 0x75, 0x69, 0x6c, 0x64, 0x49, 0x6e, 0x66, 0x6f,
0x42, 0x35, 0x5a, 0x33, 0x67, 0x69, 0x74, 0x2e, 0x6e, 0x65, 0x74, 0x66, 0x6c, 0x75, 0x78, 0x2e,
0x69, 0x6f, 0x2f, 0x72, 0x6f, 0x62, 0x2f, 0x6f, 0x63, 0x74, 0x6f, 0x70, 0x6c, 0x65, 0x78, 0x2f,
0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x2f, 0x67, 0x65, 0x6e, 0x65, 0x72, 0x61, 0x74,
0x65, 0x64, 0x2f, 0x67, 0x72, 0x70, 0x63, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33,
}
var (
file_domain_proto_rawDescOnce sync.Once
file_domain_proto_rawDescData = file_domain_proto_rawDesc
)
func file_domain_proto_rawDescGZIP() []byte {
file_domain_proto_rawDescOnce.Do(func() {
file_domain_proto_rawDescData = protoimpl.X.CompressGZIP(file_domain_proto_rawDescData)
})
return file_domain_proto_rawDescData
}
var file_domain_proto_enumTypes = make([]protoimpl.EnumInfo, 1)
var file_domain_proto_msgTypes = make([]protoimpl.MessageInfo, 5)
var file_domain_proto_goTypes = []interface{}{
(Destination_Status)(0), // 0: api.Destination.Status
(*Container)(nil), // 1: api.Container
(*Source)(nil), // 2: api.Source
(*Destination)(nil), // 3: api.Destination
(*BuildInfo)(nil), // 4: api.BuildInfo
(*AppState)(nil), // 5: api.AppState
(*timestamppb.Timestamp)(nil), // 6: google.protobuf.Timestamp
}
var file_domain_proto_depIdxs = []int32{
6, // 0: api.Container.rx_since:type_name -> google.protobuf.Timestamp
1, // 1: api.Source.container:type_name -> api.Container
6, // 2: api.Source.live_changed_at:type_name -> google.protobuf.Timestamp
1, // 3: api.Destination.container:type_name -> api.Container
0, // 4: api.Destination.status:type_name -> api.Destination.Status
2, // 5: api.AppState.source:type_name -> api.Source
3, // 6: api.AppState.destinations:type_name -> api.Destination
4, // 7: api.AppState.build_info:type_name -> api.BuildInfo
8, // [8:8] is the sub-list for method output_type
8, // [8:8] is the sub-list for method input_type
8, // [8:8] is the sub-list for extension type_name
8, // [8:8] is the sub-list for extension extendee
0, // [0:8] is the sub-list for field type_name
}
func init() { file_domain_proto_init() }
func file_domain_proto_init() {
if File_domain_proto != nil {
return
}
if !protoimpl.UnsafeEnabled {
file_domain_proto_msgTypes[0].Exporter = func(v interface{}, i int) interface{} {
switch v := v.(*Container); i {
case 0:
return &v.state
case 1:
return &v.sizeCache
case 2:
return &v.unknownFields
default:
return nil
}
}
file_domain_proto_msgTypes[1].Exporter = func(v interface{}, i int) interface{} {
switch v := v.(*Source); i {
case 0:
return &v.state
case 1:
return &v.sizeCache
case 2:
return &v.unknownFields
default:
return nil
}
}
file_domain_proto_msgTypes[2].Exporter = func(v interface{}, i int) interface{} {
switch v := v.(*Destination); i {
case 0:
return &v.state
case 1:
return &v.sizeCache
case 2:
return &v.unknownFields
default:
return nil
}
}
file_domain_proto_msgTypes[3].Exporter = func(v interface{}, i int) interface{} {
switch v := v.(*BuildInfo); i {
case 0:
return &v.state
case 1:
return &v.sizeCache
case 2:
return &v.unknownFields
default:
return nil
}
}
file_domain_proto_msgTypes[4].Exporter = func(v interface{}, i int) interface{} {
switch v := v.(*AppState); i {
case 0:
return &v.state
case 1:
return &v.sizeCache
case 2:
return &v.unknownFields
default:
return nil
}
}
}
file_domain_proto_msgTypes[0].OneofWrappers = []interface{}{}
type x struct{}
out := protoimpl.TypeBuilder{
File: protoimpl.DescBuilder{
GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
RawDescriptor: file_domain_proto_rawDesc,
NumEnums: 1,
NumMessages: 5,
NumExtensions: 0,
NumServices: 0,
},
GoTypes: file_domain_proto_goTypes,
DependencyIndexes: file_domain_proto_depIdxs,
EnumInfos: file_domain_proto_enumTypes,
MessageInfos: file_domain_proto_msgTypes,
}.Build()
File_domain_proto = out.File
file_domain_proto_rawDesc = nil
file_domain_proto_goTypes = nil
file_domain_proto_depIdxs = nil
}

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,110 @@
package protocol
import (
"git.netflux.io/rob/octoplex/internal/event"
pb "git.netflux.io/rob/octoplex/internal/generated/grpc"
)
// CommandToProto converts a command to a protobuf message.
func CommandToProto(command event.Command) *pb.Command {
switch evt := command.(type) {
case event.CommandAddDestination:
return buildAddDestinationCommand(evt)
case event.CommandRemoveDestination:
return buildRemoveDestinationCommand(evt)
case event.CommandStartDestination:
return buildStartDestinationCommand(evt)
case event.CommandStopDestination:
return buildStopDestinationCommand(evt)
case event.CommandCloseOtherInstance:
return buildCloseOtherInstanceCommand(evt)
case event.CommandKillServer:
return buildKillServerCommand(evt)
default:
panic("unknown command type")
}
}
func buildAddDestinationCommand(cmd event.CommandAddDestination) *pb.Command {
return &pb.Command{CommandType: &pb.Command_AddDestination{AddDestination: &pb.AddDestinationCommand{Name: cmd.DestinationName, Url: cmd.URL}}}
}
func buildRemoveDestinationCommand(cmd event.CommandRemoveDestination) *pb.Command {
return &pb.Command{CommandType: &pb.Command_RemoveDestination{RemoveDestination: &pb.RemoveDestinationCommand{Url: cmd.URL}}}
}
func buildStartDestinationCommand(cmd event.CommandStartDestination) *pb.Command {
return &pb.Command{CommandType: &pb.Command_StartDestination{StartDestination: &pb.StartDestinationCommand{Url: cmd.URL}}}
}
func buildStopDestinationCommand(cmd event.CommandStopDestination) *pb.Command {
return &pb.Command{CommandType: &pb.Command_StopDestination{StopDestination: &pb.StopDestinationCommand{Url: cmd.URL}}}
}
func buildCloseOtherInstanceCommand(event.CommandCloseOtherInstance) *pb.Command {
return &pb.Command{CommandType: &pb.Command_CloseOtherInstances{CloseOtherInstances: &pb.CloseOtherInstancesCommand{}}}
}
func buildKillServerCommand(event.CommandKillServer) *pb.Command {
return &pb.Command{CommandType: &pb.Command_KillServer{KillServer: &pb.KillServerCommand{}}}
}
// CommandFromProto converts a protobuf message to a command.
func CommandFromProto(pbCmd *pb.Command) event.Command {
if pbCmd == nil || pbCmd.CommandType == nil {
panic("invalid or nil pb.Command")
}
switch cmd := pbCmd.CommandType.(type) {
case *pb.Command_AddDestination:
return parseAddDestinationCommand(cmd.AddDestination)
case *pb.Command_RemoveDestination:
return parseRemoveDestinationCommand(cmd.RemoveDestination)
case *pb.Command_StartDestination:
return parseStartDestinationCommand(cmd.StartDestination)
case *pb.Command_StopDestination:
return parseStopDestinationCommand(cmd.StopDestination)
case *pb.Command_CloseOtherInstances:
return parseCloseOtherInstanceCommand(cmd.CloseOtherInstances)
case *pb.Command_KillServer:
return parseKillServerCommand(cmd.KillServer)
default:
panic("unknown pb.Command type")
}
}
func parseAddDestinationCommand(cmd *pb.AddDestinationCommand) event.Command {
if cmd == nil {
panic("nil AddDestinationCommand")
}
return event.CommandAddDestination{DestinationName: cmd.Name, URL: cmd.Url}
}
func parseRemoveDestinationCommand(cmd *pb.RemoveDestinationCommand) event.Command {
if cmd == nil {
panic("nil RemoveDestinationCommand")
}
return event.CommandRemoveDestination{URL: cmd.Url}
}
func parseStartDestinationCommand(cmd *pb.StartDestinationCommand) event.Command {
if cmd == nil {
panic("nil StartDestinationCommand")
}
return event.CommandStartDestination{URL: cmd.Url}
}
func parseStopDestinationCommand(cmd *pb.StopDestinationCommand) event.Command {
if cmd == nil {
panic("nil StopDestinationCommand")
}
return event.CommandStopDestination{URL: cmd.Url}
}
func parseCloseOtherInstanceCommand(_ *pb.CloseOtherInstancesCommand) event.Command {
return event.CommandCloseOtherInstance{}
}
func parseKillServerCommand(_ *pb.KillServerCommand) event.Command {
return event.CommandKillServer{}
}

View File

@ -0,0 +1,180 @@
package protocol_test
import (
"testing"
"git.netflux.io/rob/octoplex/internal/event"
pb "git.netflux.io/rob/octoplex/internal/generated/grpc"
"git.netflux.io/rob/octoplex/internal/protocol"
gocmp "github.com/google/go-cmp/cmp"
"github.com/stretchr/testify/assert"
"google.golang.org/protobuf/testing/protocmp"
)
func TestCommandToProto(t *testing.T) {
testCases := []struct {
name string
in event.Command
want *pb.Command
}{
{
name: "AddDestination",
in: event.CommandAddDestination{
DestinationName: "test",
URL: "rtmp://rtmp.example.com",
},
want: &pb.Command{
CommandType: &pb.Command_AddDestination{
AddDestination: &pb.AddDestinationCommand{
Name: "test",
Url: "rtmp://rtmp.example.com",
},
},
},
},
{
name: "RemoveDestination",
in: event.CommandRemoveDestination{
URL: "rtmp://remove.example.com",
},
want: &pb.Command{
CommandType: &pb.Command_RemoveDestination{
RemoveDestination: &pb.RemoveDestinationCommand{
Url: "rtmp://remove.example.com",
},
},
},
},
{
name: "StartDestination",
in: event.CommandStartDestination{
URL: "rtmp://start.example.com",
},
want: &pb.Command{
CommandType: &pb.Command_StartDestination{
StartDestination: &pb.StartDestinationCommand{
Url: "rtmp://start.example.com",
},
},
},
},
{
name: "StopDestination",
in: event.CommandStopDestination{
URL: "rtmp://stop.example.com",
},
want: &pb.Command{
CommandType: &pb.Command_StopDestination{
StopDestination: &pb.StopDestinationCommand{
Url: "rtmp://stop.example.com",
},
},
},
},
{
name: "CloseOtherInstance",
in: event.CommandCloseOtherInstance{},
want: &pb.Command{
CommandType: &pb.Command_CloseOtherInstances{
CloseOtherInstances: &pb.CloseOtherInstancesCommand{},
},
},
},
{
name: "KillServer",
in: event.CommandKillServer{},
want: &pb.Command{
CommandType: &pb.Command_KillServer{
KillServer: &pb.KillServerCommand{},
},
},
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
assert.Empty(t, gocmp.Diff(tc.want, protocol.CommandToProto(tc.in), protocmp.Transform()))
})
}
}
func TestCommandFromProto(t *testing.T) {
testCases := []struct {
name string
in *pb.Command
want event.Command
}{
{
name: "AddDestination",
in: &pb.Command{
CommandType: &pb.Command_AddDestination{
AddDestination: &pb.AddDestinationCommand{
Name: "test",
Url: "rtmp://rtmp.example.com",
},
},
},
want: event.CommandAddDestination{
DestinationName: "test",
URL: "rtmp://rtmp.example.com",
},
},
{
name: "RemoveDestination",
in: &pb.Command{
CommandType: &pb.Command_RemoveDestination{
RemoveDestination: &pb.RemoveDestinationCommand{
Url: "rtmp://remove.example.com",
},
},
},
want: event.CommandRemoveDestination{URL: "rtmp://remove.example.com"},
},
{
name: "StartDestination",
in: &pb.Command{
CommandType: &pb.Command_StartDestination{
StartDestination: &pb.StartDestinationCommand{
Url: "rtmp://start.example.com",
},
},
},
want: event.CommandStartDestination{URL: "rtmp://start.example.com"},
},
{
name: "StopDestination",
in: &pb.Command{
CommandType: &pb.Command_StopDestination{
StopDestination: &pb.StopDestinationCommand{
Url: "rtmp://stop.example.com",
},
},
},
want: event.CommandStopDestination{URL: "rtmp://stop.example.com"},
},
{
name: "CloseOtherInstance",
in: &pb.Command{
CommandType: &pb.Command_CloseOtherInstances{
CloseOtherInstances: &pb.CloseOtherInstancesCommand{},
},
},
want: event.CommandCloseOtherInstance{},
},
{
name: "KillServer",
in: &pb.Command{
CommandType: &pb.Command_KillServer{
KillServer: &pb.KillServerCommand{},
},
},
want: event.CommandKillServer{},
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
assert.Empty(t, gocmp.Diff(tc.want, protocol.CommandFromProto(tc.in)))
})
}
}

143
internal/protocol/domain.go Normal file
View File

@ -0,0 +1,143 @@
package protocol
import (
"errors"
"time"
"git.netflux.io/rob/octoplex/internal/domain"
pb "git.netflux.io/rob/octoplex/internal/generated/grpc"
"google.golang.org/protobuf/types/known/timestamppb"
)
// ContainerToProto converts a domain.Container to a protobuf Container.
func ContainerToProto(c domain.Container) *pb.Container {
var errString string
if c.Err != nil {
errString = c.Err.Error()
}
var exitCode *int32
if c.ExitCode != nil {
code := int32(*c.ExitCode)
exitCode = &code
}
var rxSince *timestamppb.Timestamp
if !c.RxSince.IsZero() {
rxSince = timestamppb.New(c.RxSince)
}
return &pb.Container{
Id: c.ID,
Status: c.Status,
HealthState: c.HealthState,
CpuPercent: c.CPUPercent,
MemoryUsageBytes: c.MemoryUsageBytes,
RxRate: int32(c.RxRate),
TxRate: int32(c.TxRate),
RxSince: rxSince,
ImageName: c.ImageName,
PullStatus: c.PullStatus,
PullProgress: c.PullProgress,
PullPercent: int32(c.PullPercent),
RestartCount: int32(c.RestartCount),
ExitCode: exitCode,
Err: errString,
}
}
// ContainerFromProto converts a protobuf Container to a domain.Container.
func ContainerFromProto(pbContainer *pb.Container) domain.Container {
if pbContainer == nil {
return domain.Container{}
}
var exitCode *int
if pbContainer.ExitCode != nil {
val := int(*pbContainer.ExitCode)
exitCode = &val
}
var err error
if pbContainer.Err != "" {
err = errors.New(pbContainer.Err)
}
var rxSince time.Time
if pbContainer.RxSince != nil {
rxSince = pbContainer.RxSince.AsTime()
}
return domain.Container{
ID: pbContainer.Id,
Status: pbContainer.Status,
HealthState: pbContainer.HealthState,
CPUPercent: pbContainer.CpuPercent,
MemoryUsageBytes: pbContainer.MemoryUsageBytes,
RxRate: int(pbContainer.RxRate),
TxRate: int(pbContainer.TxRate),
RxSince: rxSince,
ImageName: pbContainer.ImageName,
PullStatus: pbContainer.PullStatus,
PullProgress: pbContainer.PullProgress,
PullPercent: int(pbContainer.PullPercent),
RestartCount: int(pbContainer.RestartCount),
ExitCode: exitCode,
Err: err,
}
}
// DestinationsToProto converts a slice of domain.Destinations to a slice of
// protobuf Destinations.
func DestinationsToProto(inDests []domain.Destination) []*pb.Destination {
destinations := make([]*pb.Destination, 0, len(inDests))
for _, d := range inDests {
destinations = append(destinations, DestinationToProto(d))
}
return destinations
}
// DestinationToProto converts a domain.Destination to a protobuf Destination.
func DestinationToProto(d domain.Destination) *pb.Destination {
return &pb.Destination{
Container: ContainerToProto(d.Container),
Status: DestinationStatusToProto(d.Status),
Name: d.Name,
Url: d.URL,
}
}
// ProtoToDestinations converts a slice of protobuf Destinations to a slice of
// domain.Destinations.
func ProtoToDestinations(pbDests []*pb.Destination) []domain.Destination {
if pbDests == nil {
return nil
}
dests := make([]domain.Destination, 0, len(pbDests))
for _, pbDest := range pbDests {
if pbDest == nil {
continue
}
dests = append(dests, domain.Destination{
Container: ContainerFromProto(pbDest.Container),
Status: domain.DestinationStatus(pbDest.Status),
Name: pbDest.Name,
URL: pbDest.Url,
})
}
return dests
}
// DestinationStatusToProto converts a domain.DestinationStatus to a
// pb.Destination_Status.
func DestinationStatusToProto(s domain.DestinationStatus) pb.Destination_Status {
switch s {
case domain.DestinationStatusStarting:
return pb.Destination_STATUS_STARTING
case domain.DestinationStatusLive:
return pb.Destination_STATUS_LIVE
default:
return pb.Destination_STATUS_OFF_AIR
}
}

View File

@ -0,0 +1,222 @@
package protocol_test
import (
"errors"
"testing"
"time"
"git.netflux.io/rob/octoplex/internal/domain"
pb "git.netflux.io/rob/octoplex/internal/generated/grpc"
"git.netflux.io/rob/octoplex/internal/protocol"
gocmp "github.com/google/go-cmp/cmp"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"google.golang.org/protobuf/testing/protocmp"
"google.golang.org/protobuf/types/known/timestamppb"
)
func TestContainerToProto(t *testing.T) {
exitCode := 1
ts := time.Unix(1234567890, 0)
testCases := []struct {
name string
in domain.Container
want *pb.Container
}{
{
name: "complete",
in: domain.Container{
ID: "abc123",
Status: "running",
HealthState: "healthy",
CPUPercent: 12.5,
MemoryUsageBytes: 2048,
RxRate: 100,
TxRate: 200,
RxSince: ts,
ImageName: "nginx",
PullStatus: "pulling",
PullProgress: "50%",
PullPercent: 50,
RestartCount: 3,
ExitCode: &exitCode,
Err: errors.New("container error"),
},
want: &pb.Container{
Id: "abc123",
Status: "running",
HealthState: "healthy",
CpuPercent: 12.5,
MemoryUsageBytes: 2048,
RxRate: 100,
TxRate: 200,
RxSince: timestamppb.New(ts),
ImageName: "nginx",
PullStatus: "pulling",
PullProgress: "50%",
PullPercent: 50,
RestartCount: 3,
ExitCode: protoInt32(1),
Err: "container error",
},
},
{
name: "zero values",
in: domain.Container{},
want: &pb.Container{},
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
assert.Empty(t, gocmp.Diff(tc.want, protocol.ContainerToProto(tc.in), protocmp.Transform()))
})
}
}
func TestContainerFromProto(t *testing.T) {
ts := timestamppb.New(time.Now())
exitCode := int32(2)
testCases := []struct {
name string
in *pb.Container
want domain.Container
}{
{
name: "complete",
in: &pb.Container{
Id: "xyz789",
Status: "exited",
HealthState: "unhealthy",
CpuPercent: 42.0,
MemoryUsageBytes: 4096,
RxRate: 300,
TxRate: 400,
RxSince: ts,
ImageName: "redis",
PullStatus: "complete",
PullProgress: "100%",
PullPercent: 100,
RestartCount: 1,
ExitCode: &exitCode,
Err: "crash error",
},
want: domain.Container{
ID: "xyz789",
Status: "exited",
HealthState: "unhealthy",
CPUPercent: 42.0,
MemoryUsageBytes: 4096,
RxRate: 300,
TxRate: 400,
RxSince: ts.AsTime(),
ImageName: "redis",
PullStatus: "complete",
PullProgress: "100%",
PullPercent: 100,
RestartCount: 1,
ExitCode: protoInt(2),
Err: errors.New("crash error"),
},
},
{
name: "nil proto",
in: nil,
want: domain.Container{},
},
{
name: "zero values",
in: &pb.Container{},
want: domain.Container{},
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
got := protocol.ContainerFromProto(tc.in)
assert.Empty(
t,
gocmp.Diff(
tc.want,
got,
gocmp.Comparer(compareErrorMessages),
))
})
}
}
func TestDestinationConversions(t *testing.T) {
testCases := []struct {
name string
in domain.Destination
want *pb.Destination
}{
{
name: "basic destination",
in: domain.Destination{
Name: "dest1",
URL: "rtmp://dest1",
Status: domain.DestinationStatusLive,
Container: domain.Container{ID: "c1"},
},
want: &pb.Destination{
Name: "dest1",
Url: "rtmp://dest1",
Status: pb.Destination_STATUS_LIVE,
Container: &pb.Container{Id: "c1"},
},
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
proto := protocol.DestinationToProto(tc.in)
assert.Equal(t, tc.want.Name, proto.Name)
assert.Equal(t, tc.want.Url, proto.Url)
assert.Equal(t, tc.want.Status, proto.Status)
require.NotNil(t, proto.Container)
assert.Equal(t, tc.want.Container.Id, proto.Container.Id)
dests := protocol.ProtoToDestinations([]*pb.Destination{proto})
assert.Len(t, dests, 1)
assert.Equal(t, tc.in.Name, dests[0].Name)
assert.Equal(t, tc.in.URL, dests[0].URL)
assert.Equal(t, tc.in.Status, dests[0].Status)
assert.Equal(t, tc.in.Container.ID, dests[0].Container.ID)
})
}
}
func TestDestinationStatusToProto(t *testing.T) {
testCases := []struct {
name string
in domain.DestinationStatus
want pb.Destination_Status
}{
{"Starting", domain.DestinationStatusStarting, pb.Destination_STATUS_STARTING},
{"Live", domain.DestinationStatusLive, pb.Destination_STATUS_LIVE},
{"Off-air", domain.DestinationStatusOffAir, pb.Destination_STATUS_OFF_AIR},
{"Unknown", domain.DestinationStatus(999), pb.Destination_STATUS_OFF_AIR},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
assert.Equal(t, tc.want, protocol.DestinationStatusToProto(tc.in))
})
}
}
func protoInt32(v int32) *int32 { return &v }
func protoInt(v int) *int { return &v }
// compareErrorMessages compares two error messages for equality using only the
// error message string.
func compareErrorMessages(x, y error) bool {
if x == nil || y == nil {
return x == y
}
return x.Error() == y.Error()
}

263
internal/protocol/event.go Normal file
View File

@ -0,0 +1,263 @@
package protocol
import (
"errors"
"time"
"git.netflux.io/rob/octoplex/internal/domain"
"git.netflux.io/rob/octoplex/internal/event"
pb "git.netflux.io/rob/octoplex/internal/generated/grpc"
"google.golang.org/protobuf/types/known/timestamppb"
)
// EventToProto converts an event to a protobuf message.
func EventToProto(ev event.Event) *pb.Event {
switch evt := ev.(type) {
case event.AppStateChangedEvent:
return buildAppStateChangeEvent(evt)
case event.DestinationAddedEvent:
return buildDestinationAddedEvent(evt)
case event.AddDestinationFailedEvent:
return buildAddDestinationFailedEvent(evt)
case event.DestinationStreamExitedEvent:
return buildDestinationStreamExitedEvent(evt)
case event.StartDestinationFailedEvent:
return buildStartDestinationFailedEvent(evt)
case event.DestinationRemovedEvent:
return buildDestinationRemovedEvent(evt)
case event.RemoveDestinationFailedEvent:
return buildRemoveDestinationFailedEvent(evt)
case event.FatalErrorOccurredEvent:
return buildFatalErrorOccurredEvent(evt)
case event.OtherInstanceDetectedEvent:
return buildOtherInstanceDetectedEvent(evt)
case event.MediaServerStartedEvent:
return buildMediaServerStartedEvent(evt)
default:
panic("unknown event type")
}
}
func buildAppStateChangeEvent(evt event.AppStateChangedEvent) *pb.Event {
var liveChangedAt *timestamppb.Timestamp
if !evt.State.Source.LiveChangedAt.IsZero() {
liveChangedAt = timestamppb.New(evt.State.Source.LiveChangedAt)
}
return &pb.Event{
EventType: &pb.Event_AppStateChanged{
AppStateChanged: &pb.AppStateChangedEvent{
AppState: &pb.AppState{
Source: &pb.Source{
Container: ContainerToProto(evt.State.Source.Container),
Live: evt.State.Source.Live,
LiveChangedAt: liveChangedAt,
Tracks: evt.State.Source.Tracks,
ExitReason: evt.State.Source.ExitReason,
},
Destinations: DestinationsToProto(evt.State.Destinations),
BuildInfo: &pb.BuildInfo{
GoVersion: evt.State.BuildInfo.GoVersion,
Version: evt.State.BuildInfo.Version,
Commit: evt.State.BuildInfo.Commit,
Date: evt.State.BuildInfo.Date,
},
},
},
},
}
}
func buildDestinationAddedEvent(evt event.DestinationAddedEvent) *pb.Event {
return &pb.Event{
EventType: &pb.Event_DestinationAdded{
DestinationAdded: &pb.DestinationAddedEvent{Url: evt.URL},
},
}
}
func buildAddDestinationFailedEvent(evt event.AddDestinationFailedEvent) *pb.Event {
return &pb.Event{
EventType: &pb.Event_AddDestinationFailed{
AddDestinationFailed: &pb.AddDestinationFailedEvent{Url: evt.URL, Error: evt.Err.Error()},
},
}
}
func buildDestinationStreamExitedEvent(evt event.DestinationStreamExitedEvent) *pb.Event {
return &pb.Event{
EventType: &pb.Event_DestinationStreamExited{
DestinationStreamExited: &pb.DestinationStreamExitedEvent{Name: evt.Name, Error: evt.Err.Error()},
},
}
}
func buildStartDestinationFailedEvent(evt event.StartDestinationFailedEvent) *pb.Event {
return &pb.Event{
EventType: &pb.Event_StartDestinationFailed{
StartDestinationFailed: &pb.StartDestinationFailedEvent{Url: evt.URL, Message: evt.Message},
},
}
}
func buildDestinationRemovedEvent(evt event.DestinationRemovedEvent) *pb.Event {
return &pb.Event{
EventType: &pb.Event_DestinationRemoved{
DestinationRemoved: &pb.DestinationRemovedEvent{Url: evt.URL},
},
}
}
func buildRemoveDestinationFailedEvent(evt event.RemoveDestinationFailedEvent) *pb.Event {
return &pb.Event{
EventType: &pb.Event_RemoveDestinationFailed{
RemoveDestinationFailed: &pb.RemoveDestinationFailedEvent{Url: evt.URL, Error: evt.Err.Error()},
},
}
}
func buildFatalErrorOccurredEvent(evt event.FatalErrorOccurredEvent) *pb.Event {
return &pb.Event{
EventType: &pb.Event_FatalError{
FatalError: &pb.FatalErrorEvent{Message: evt.Message},
},
}
}
func buildOtherInstanceDetectedEvent(_ event.OtherInstanceDetectedEvent) *pb.Event {
return &pb.Event{
EventType: &pb.Event_OtherInstanceDetected{
OtherInstanceDetected: &pb.OtherInstanceDetectedEvent{},
},
}
}
func buildMediaServerStartedEvent(evt event.MediaServerStartedEvent) *pb.Event {
return &pb.Event{
EventType: &pb.Event_MediaServerStarted{
MediaServerStarted: &pb.MediaServerStartedEvent{RtmpUrl: evt.RTMPURL, RtmpsUrl: evt.RTMPSURL},
},
}
}
// EventFromProto converts a protobuf message to an event.
func EventFromProto(pbEv *pb.Event) event.Event {
if pbEv == nil || pbEv.EventType == nil {
panic("invalid or nil pb.Event")
}
switch evt := pbEv.EventType.(type) {
case *pb.Event_AppStateChanged:
return parseAppStateChangedEvent(evt.AppStateChanged)
case *pb.Event_DestinationAdded:
return parseDestinationAddedEvent(evt.DestinationAdded)
case *pb.Event_AddDestinationFailed:
return parseAddDestinationFailedEvent(evt.AddDestinationFailed)
case *pb.Event_DestinationStreamExited:
return parseDestinationStreamExitedEvent(evt.DestinationStreamExited)
case *pb.Event_StartDestinationFailed:
return parseStartDestinationFailedEvent(evt.StartDestinationFailed)
case *pb.Event_DestinationRemoved:
return parseDestinationRemovedEvent(evt.DestinationRemoved)
case *pb.Event_RemoveDestinationFailed:
return parseRemoveDestinationFailedEvent(evt.RemoveDestinationFailed)
case *pb.Event_FatalError:
return parseFatalErrorOccurredEvent(evt.FatalError)
case *pb.Event_OtherInstanceDetected:
return parseOtherInstanceDetectedEvent(evt.OtherInstanceDetected)
case *pb.Event_MediaServerStarted:
return parseMediaServerStartedEvent(evt.MediaServerStarted)
default:
panic("unknown pb.Event type")
}
}
func parseAppStateChangedEvent(evt *pb.AppStateChangedEvent) event.Event {
if evt == nil || evt.AppState == nil || evt.AppState.Source == nil {
panic("invalid AppStateChangedEvent")
}
var liveChangedAt time.Time
if evt.AppState.Source.LiveChangedAt != nil {
liveChangedAt = evt.AppState.Source.LiveChangedAt.AsTime()
}
return event.AppStateChangedEvent{
State: domain.AppState{
Source: domain.Source{
Container: ContainerFromProto(evt.AppState.Source.Container),
Live: evt.AppState.Source.Live,
LiveChangedAt: liveChangedAt,
Tracks: evt.AppState.Source.Tracks,
ExitReason: evt.AppState.Source.ExitReason,
},
Destinations: ProtoToDestinations(evt.AppState.Destinations),
BuildInfo: domain.BuildInfo{
GoVersion: evt.AppState.BuildInfo.GoVersion,
Version: evt.AppState.BuildInfo.Version,
Commit: evt.AppState.BuildInfo.Commit,
Date: evt.AppState.BuildInfo.Date,
},
},
}
}
func parseDestinationAddedEvent(evt *pb.DestinationAddedEvent) event.Event {
if evt == nil {
panic("nil DestinationAddedEvent")
}
return event.DestinationAddedEvent{URL: evt.Url}
}
func parseAddDestinationFailedEvent(evt *pb.AddDestinationFailedEvent) event.Event {
if evt == nil {
panic("nil AddDestinationFailedEvent")
}
return event.AddDestinationFailedEvent{URL: evt.Url, Err: errors.New(evt.Error)}
}
func parseDestinationStreamExitedEvent(evt *pb.DestinationStreamExitedEvent) event.Event {
if evt == nil {
panic("nil DestinationStreamExitedEvent")
}
return event.DestinationStreamExitedEvent{Name: evt.Name, Err: errors.New(evt.Error)}
}
func parseStartDestinationFailedEvent(evt *pb.StartDestinationFailedEvent) event.Event {
if evt == nil {
panic("nil StartDestinationFailedEvent")
}
return event.StartDestinationFailedEvent{URL: evt.Url, Message: evt.Message}
}
func parseDestinationRemovedEvent(evt *pb.DestinationRemovedEvent) event.Event {
if evt == nil {
panic("nil DestinationRemovedEvent")
}
return event.DestinationRemovedEvent{URL: evt.Url}
}
func parseRemoveDestinationFailedEvent(evt *pb.RemoveDestinationFailedEvent) event.Event {
if evt == nil {
panic("nil RemoveDestinationFailedEvent")
}
return event.RemoveDestinationFailedEvent{URL: evt.Url, Err: errors.New(evt.Error)}
}
func parseFatalErrorOccurredEvent(evt *pb.FatalErrorEvent) event.Event {
if evt == nil {
panic("nil FatalErrorEvent")
}
return event.FatalErrorOccurredEvent{Message: evt.Message}
}
func parseOtherInstanceDetectedEvent(_ *pb.OtherInstanceDetectedEvent) event.Event {
return event.OtherInstanceDetectedEvent{}
}
func parseMediaServerStartedEvent(evt *pb.MediaServerStartedEvent) event.Event {
if evt == nil {
panic("nil MediaServerStartedEvent")
}
return event.MediaServerStartedEvent{RTMPURL: evt.RtmpUrl, RTMPSURL: evt.RtmpsUrl}
}

View File

@ -0,0 +1,267 @@
package protocol_test
import (
"errors"
"testing"
"git.netflux.io/rob/octoplex/internal/domain"
"git.netflux.io/rob/octoplex/internal/event"
pb "git.netflux.io/rob/octoplex/internal/generated/grpc"
"git.netflux.io/rob/octoplex/internal/protocol"
"github.com/google/go-cmp/cmp"
gocmp "github.com/google/go-cmp/cmp"
"github.com/stretchr/testify/assert"
"google.golang.org/protobuf/testing/protocmp"
)
func TestEventToProto(t *testing.T) {
testCases := []struct {
name string
in event.Event
want *pb.Event
}{
{
name: "AppStateChanged",
in: event.AppStateChangedEvent{
State: domain.AppState{
Source: domain.Source{
Container: domain.Container{
ID: "abc123",
},
Live: true,
},
Destinations: []domain.Destination{
{
Name: "dest1",
URL: "rtmp://dest1.example.com",
Container: domain.Container{
ID: "bcd456",
},
},
},
BuildInfo: domain.BuildInfo{GoVersion: "go1.16", Version: "v1.0.0"},
},
},
want: &pb.Event{
EventType: &pb.Event_AppStateChanged{
AppStateChanged: &pb.AppStateChangedEvent{
AppState: &pb.AppState{
Source: &pb.Source{
Container: &pb.Container{
Id: "abc123",
},
Live: true,
},
Destinations: []*pb.Destination{
{
Name: "dest1",
Url: "rtmp://dest1.example.com",
Container: &pb.Container{
Id: "bcd456",
},
},
},
BuildInfo: &pb.BuildInfo{GoVersion: "go1.16", Version: "v1.0.0"},
},
},
},
},
},
{
name: "DestinationAdded",
in: event.DestinationAddedEvent{URL: "rtmp://dest.example.com"},
want: &pb.Event{
EventType: &pb.Event_DestinationAdded{
DestinationAdded: &pb.DestinationAddedEvent{
Url: "rtmp://dest.example.com",
},
},
},
},
{
name: "AddDestinationFailed",
in: event.AddDestinationFailedEvent{URL: "rtmp://fail.example.com", Err: errors.New("failed")},
want: &pb.Event{
EventType: &pb.Event_AddDestinationFailed{
AddDestinationFailed: &pb.AddDestinationFailedEvent{
Url: "rtmp://fail.example.com",
Error: "failed",
},
},
},
},
{
name: "DestinationStreamExited",
in: event.DestinationStreamExitedEvent{Name: "stream1", Err: errors.New("exit reason")},
want: &pb.Event{
EventType: &pb.Event_DestinationStreamExited{
DestinationStreamExited: &pb.DestinationStreamExitedEvent{
Name: "stream1",
Error: "exit reason",
},
},
},
},
{
name: "FatalErrorOccurred",
in: event.FatalErrorOccurredEvent{Message: "fatal error"},
want: &pb.Event{
EventType: &pb.Event_FatalError{
FatalError: &pb.FatalErrorEvent{Message: "fatal error"},
},
},
},
{
name: "OtherInstanceDetected",
in: event.OtherInstanceDetectedEvent{},
want: &pb.Event{
EventType: &pb.Event_OtherInstanceDetected{
OtherInstanceDetected: &pb.OtherInstanceDetectedEvent{},
},
},
},
{
name: "MediaServerStarted",
in: event.MediaServerStartedEvent{RTMPURL: "rtmp://media", RTMPSURL: "rtmps://media"},
want: &pb.Event{
EventType: &pb.Event_MediaServerStarted{
MediaServerStarted: &pb.MediaServerStartedEvent{
RtmpUrl: "rtmp://media",
RtmpsUrl: "rtmps://media",
},
},
},
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
assert.Empty(t, gocmp.Diff(tc.want, protocol.EventToProto(tc.in), protocmp.Transform()))
})
}
}
func TestEventFromProto(t *testing.T) {
testCases := []struct {
name string
in *pb.Event
want event.Event
}{
{
name: "AppStateChanged",
in: &pb.Event{
EventType: &pb.Event_AppStateChanged{
AppStateChanged: &pb.AppStateChangedEvent{
AppState: &pb.AppState{
Source: &pb.Source{
Container: &pb.Container{Id: "abc123"},
Live: true,
},
Destinations: []*pb.Destination{
{
Name: "dest1",
Url: "rtmp://dest1.example.com",
Container: &pb.Container{Id: "bcd456"},
},
},
BuildInfo: &pb.BuildInfo{
GoVersion: "go1.16",
Version: "v1.0.0",
},
},
},
},
},
want: event.AppStateChangedEvent{
State: domain.AppState{
Source: domain.Source{
Container: domain.Container{ID: "abc123"},
Live: true,
},
Destinations: []domain.Destination{
{
Name: "dest1",
URL: "rtmp://dest1.example.com",
Container: domain.Container{ID: "bcd456"},
},
},
BuildInfo: domain.BuildInfo{
GoVersion: "go1.16",
Version: "v1.0.0",
},
},
},
},
{
name: "DestinationAdded",
in: &pb.Event{
EventType: &pb.Event_DestinationAdded{
DestinationAdded: &pb.DestinationAddedEvent{
Url: "rtmp://dest.example.com",
},
},
},
want: event.DestinationAddedEvent{URL: "rtmp://dest.example.com"},
},
{
name: "AddDestinationFailed",
in: &pb.Event{
EventType: &pb.Event_AddDestinationFailed{
AddDestinationFailed: &pb.AddDestinationFailedEvent{
Url: "rtmp://fail.example.com",
Error: "failed",
},
},
},
want: event.AddDestinationFailedEvent{URL: "rtmp://fail.example.com", Err: errors.New("failed")},
},
{
name: "DestinationStreamExited",
in: &pb.Event{
EventType: &pb.Event_DestinationStreamExited{
DestinationStreamExited: &pb.DestinationStreamExitedEvent{
Name: "stream1",
Error: "exit reason",
},
},
},
want: event.DestinationStreamExitedEvent{Name: "stream1", Err: errors.New("exit reason")},
},
{
name: "FatalErrorOccurred",
in: &pb.Event{
EventType: &pb.Event_FatalError{
FatalError: &pb.FatalErrorEvent{Message: "fatal error"},
},
},
want: event.FatalErrorOccurredEvent{Message: "fatal error"},
},
{
name: "OtherInstanceDetected",
in: &pb.Event{
EventType: &pb.Event_OtherInstanceDetected{
OtherInstanceDetected: &pb.OtherInstanceDetectedEvent{},
},
},
want: event.OtherInstanceDetectedEvent{},
},
{
name: "MediaServerStarted",
in: &pb.Event{
EventType: &pb.Event_MediaServerStarted{
MediaServerStarted: &pb.MediaServerStartedEvent{
RtmpUrl: "rtmp://media",
RtmpsUrl: "rtmps://media",
},
},
},
want: event.MediaServerStartedEvent{RTMPURL: "rtmp://media", RTMPSURL: "rtmps://media"},
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
assert.Empty(t, cmp.Diff(tc.want, protocol.EventFromProto(tc.in), gocmp.Comparer(compareErrorMessages)))
})
}
}

147
internal/server/grpc.go Normal file
View File

@ -0,0 +1,147 @@
package server
import (
"context"
"errors"
"fmt"
"io"
"log/slog"
"sync"
"time"
"git.netflux.io/rob/octoplex/internal/event"
pb "git.netflux.io/rob/octoplex/internal/generated/grpc"
"git.netflux.io/rob/octoplex/internal/protocol"
"golang.org/x/sync/errgroup"
)
// Server is the gRPC server that handles incoming commands and outgoing
// events.
type Server struct {
pb.UnimplementedInternalAPIServer
dispatcher func(event.Command)
bus *event.Bus
logger *slog.Logger
mu sync.Mutex
clientCount int
clientC chan struct{}
}
// newServer creates a new gRPC server.
func newServer(
dispatcher func(event.Command),
bus *event.Bus,
logger *slog.Logger,
) *Server {
return &Server{
dispatcher: dispatcher,
bus: bus,
clientC: make(chan struct{}, 1),
logger: logger.With("component", "server"),
}
}
func (s *Server) Communicate(stream pb.InternalAPI_CommunicateServer) error {
g, ctx := errgroup.WithContext(stream.Context())
// perform handshake:
startHandshakeCmd, err := stream.Recv()
if err != nil {
return fmt.Errorf("receive start handshake command: %w", err)
}
if startHandshakeCmd.GetCommand() == nil || startHandshakeCmd.GetCommand().GetStartHandshake() == nil {
return fmt.Errorf("expected start handshake command but got: %T", startHandshakeCmd)
}
if err := stream.Send(&pb.Envelope{Payload: &pb.Envelope_Event{Event: &pb.Event{EventType: &pb.Event_HandshakeCompleted{}}}}); err != nil {
return fmt.Errorf("send handshake completed event: %w", err)
}
// Notify that a client has connected and completed the handshake.
select {
case s.clientC <- struct{}{}:
default:
}
g.Go(func() error {
eventsC := s.bus.Register()
defer s.bus.Deregister(eventsC)
for {
select {
case evt := <-eventsC:
if err := stream.Send(&pb.Envelope{Payload: &pb.Envelope_Event{Event: protocol.EventToProto(evt)}}); err != nil {
return fmt.Errorf("send event: %w", err)
}
case <-ctx.Done():
return ctx.Err()
}
}
})
g.Go(func() error {
s.mu.Lock()
s.clientCount++
s.mu.Unlock()
defer func() {
s.mu.Lock()
s.clientCount--
s.mu.Unlock()
}()
for {
in, err := stream.Recv()
if err == io.EOF {
s.logger.Info("Client disconnected")
return err
}
if err != nil {
return fmt.Errorf("receive message: %w", err)
}
switch pbCmd := in.Payload.(type) {
case *pb.Envelope_Command:
cmd := protocol.CommandFromProto(pbCmd.Command)
s.logger.Debug("Received command from gRPC stream", "command", cmd.Name())
s.dispatcher(cmd)
default:
return fmt.Errorf("expected command but got: %T", pbCmd)
}
}
})
if err := g.Wait(); err != nil && !errors.Is(err, io.EOF) && !errors.Is(err, context.Canceled) {
s.logger.Error("Client stream closed with error", "err", err)
return fmt.Errorf("errgroup.Wait: %w", err)
}
s.logger.Info("Client stream closed")
return nil
}
// GetClientCount returns the number of connected clients.
func (s *Server) GetClientCount() int {
s.mu.Lock()
defer s.mu.Unlock()
return s.clientCount
}
const waitForClientTimeout = 10 * time.Second
// WaitForClient waits for _any_ client to connect and complete the handshake.
// It times out if no client has connected after 10 seconds.
func (s *Server) WaitForClient(ctx context.Context) error {
select {
case <-s.clientC:
return nil
case <-time.After(waitForClientTimeout):
return errors.New("timeout")
case <-ctx.Done():
return ctx.Err()
}
}

View File

@ -1,4 +1,4 @@
package app
package server
import (
"cmp"
@ -6,6 +6,7 @@ import (
"errors"
"fmt"
"log/slog"
"net"
"slices"
"time"
@ -13,38 +14,32 @@ import (
"git.netflux.io/rob/octoplex/internal/container"
"git.netflux.io/rob/octoplex/internal/domain"
"git.netflux.io/rob/octoplex/internal/event"
pb "git.netflux.io/rob/octoplex/internal/generated/grpc"
"git.netflux.io/rob/octoplex/internal/mediaserver"
"git.netflux.io/rob/octoplex/internal/replicator"
"git.netflux.io/rob/octoplex/internal/terminal"
"github.com/docker/docker/client"
"google.golang.org/grpc"
)
// App is an instance of the app.
type App struct {
cfg config.Config
configService *config.Service
eventBus *event.Bus
dispatchC chan event.Command
dockerClient container.DockerClient
screen *terminal.Screen // Screen may be nil.
headless bool
clipboardAvailable bool
configFilePath string
buildInfo domain.BuildInfo
logger *slog.Logger
cfg config.Config
configService *config.Service
eventBus *event.Bus
dispatchC chan event.Command
dockerClient container.DockerClient
waitForClient bool
logger *slog.Logger
}
// Params holds the parameters for running the application.
type Params struct {
ConfigService *config.Service
DockerClient container.DockerClient
ChanSize int
Screen *terminal.Screen // Screen may be nil.
Headless bool
ClipboardAvailable bool
ConfigFilePath string
BuildInfo domain.BuildInfo
Logger *slog.Logger
ConfigService *config.Service
DockerClient container.DockerClient
ChanSize int
ConfigFilePath string
WaitForClient bool
Logger *slog.Logger
}
// defaultChanSize is the default size of the dispatch channel.
@ -53,17 +48,13 @@ const defaultChanSize = 64
// New creates a new application instance.
func New(params Params) *App {
return &App{
cfg: params.ConfigService.Current(),
configService: params.ConfigService,
eventBus: event.NewBus(params.Logger.With("component", "event_bus")),
dispatchC: make(chan event.Command, cmp.Or(params.ChanSize, defaultChanSize)),
dockerClient: params.DockerClient,
screen: params.Screen,
headless: params.Headless,
clipboardAvailable: params.ClipboardAvailable,
configFilePath: params.ConfigFilePath,
buildInfo: params.BuildInfo,
logger: params.Logger,
cfg: params.ConfigService.Current(),
configService: params.ConfigService,
eventBus: event.NewBus(params.Logger.With("component", "event_bus")),
dispatchC: make(chan event.Command, cmp.Or(params.ChanSize, defaultChanSize)),
dockerClient: params.DockerClient,
waitForClient: params.WaitForClient,
logger: params.Logger,
}
}
@ -78,20 +69,26 @@ func (a *App) Run(ctx context.Context) error {
return errors.New("config: either sources.mediaServer.rtmp.enabled or sources.mediaServer.rtmps.enabled must be set")
}
if !a.headless {
ui, err := terminal.StartUI(ctx, terminal.StartParams{
EventBus: a.eventBus,
Dispatcher: func(cmd event.Command) { a.dispatchC <- cmd },
Screen: a.screen,
ClipboardAvailable: a.clipboardAvailable,
ConfigFilePath: a.configFilePath,
BuildInfo: a.buildInfo,
Logger: a.logger.With("component", "ui"),
})
if err != nil {
return fmt.Errorf("start terminal user interface: %w", err)
const grpcAddr = ":50051"
lis, err := net.Listen("tcp", grpcAddr)
if err != nil {
return fmt.Errorf("listen: %w", err)
}
defer lis.Close()
grpcServer := grpc.NewServer()
grpcDone := make(chan error, 1)
internalAPI := newServer(a.DispatchAsync, a.eventBus, a.logger)
pb.RegisterInternalAPIServer(grpcServer, internalAPI)
go func() {
a.logger.Info("gRPC server started", "addr", grpcAddr)
grpcDone <- grpcServer.Serve(lis)
}()
if a.waitForClient {
if err = internalAPI.WaitForClient(ctx); err != nil {
return fmt.Errorf("wait for client: %w", err)
}
defer ui.Close()
}
// emptyUI is a dummy function that sets the UI state to an empty state, and
@ -107,12 +104,13 @@ func (a *App) Run(ctx context.Context) error {
a.eventBus.Send(event.AppStateChangedEvent{State: domain.AppState{}})
}
// doFatalError publishes a fatal error to the event bus, waiting for the
// user to acknowledge it if not in headless mode.
// doFatalError publishes a fatal error to the event bus. It will block until
// the user acknowledges it if there is 1 or more clients connected to the
// internal API.
doFatalError := func(msg string) {
a.eventBus.Send(event.FatalErrorOccurredEvent{Message: msg})
if a.headless {
if internalAPI.GetClientCount() == 0 {
return
}
@ -177,21 +175,20 @@ func (a *App) Run(ctx context.Context) error {
defer uiUpdateT.Stop()
startMediaServerC := make(chan struct{}, 1)
if a.headless { // disable startup check in headless mode for now
if ok, startupErr := doStartupCheck(ctx, containerClient, a.eventBus); startupErr != nil {
doFatalError(startupErr.Error())
return startupErr
} else if ok {
startMediaServerC <- struct{}{}
} else {
if ok, startupErr := doStartupCheck(ctx, containerClient, a.eventBus); startupErr != nil {
doFatalError(startupErr.Error())
return startupErr
} else if ok {
startMediaServerC <- struct{}{}
}
}
for {
select {
case <-ctx.Done():
return ctx.Err()
case grpcErr := <-grpcDone:
a.logger.Error("gRPC server exited", "err", grpcErr)
return grpcErr
case <-startMediaServerC:
if err = srv.Start(ctx); err != nil {
return fmt.Errorf("start mediaserver: %w", err)
@ -245,7 +242,12 @@ func (a *App) Dispatch(cmd event.Command) event.Event {
return <-ch
}
// errExit is an error that indicates the app should exit.
// DispatchAsync dispatches a command to be executed synchronously.
func (a *App) DispatchAsync(cmd event.Command) {
a.dispatchC <- cmd
}
// errExit is an error that indicates the server should exit.
var errExit = errors.New("exit")
// handleCommand handles an incoming command. It may return an Event which will
@ -253,7 +255,7 @@ var errExit = errors.New("exit")
// benefit of synchronous callers. The event may be nil. It may also publish
// other events to the event bus which are not returned. Currently the only
// error that may be returned is [errExit], which indicates to the main event
// loop that the app should exit.
// loop that the server should exit.
func (a *App) handleCommand(
ctx context.Context,
cmd event.Command,
@ -262,7 +264,7 @@ func (a *App) handleCommand(
containerClient *container.Client,
startMediaServerC chan struct{},
) (evt event.Event, _ error) {
a.logger.Debug("Command received", "cmd", cmd.Name())
a.logger.Debug("Command received in handler", "cmd", cmd.Name())
defer func() {
if evt != nil {
a.eventBus.Send(evt)
@ -272,6 +274,12 @@ func (a *App) handleCommand(
}
}()
// If the command is a syncCommand, we need to extract the command from it so
// it can be type-switched against.
if c, ok := cmd.(syncCommand); ok {
cmd = c.Command
}
switch c := cmd.(type) {
case event.CommandAddDestination:
newCfg := a.cfg
@ -281,10 +289,11 @@ func (a *App) handleCommand(
})
if err := a.configService.SetConfig(newCfg); err != nil {
a.logger.Error("Add destination failed", "err", err)
return event.AddDestinationFailedEvent{Err: err}, nil
return event.AddDestinationFailedEvent{URL: c.URL, Err: err}, nil
}
a.cfg = newCfg
a.handleConfigUpdate(state)
a.logger.Info("Destination added", "url", c.URL)
a.eventBus.Send(event.DestinationAddedEvent{URL: c.URL})
case event.CommandRemoveDestination:
repl.StopDestination(c.URL) // no-op if not live
@ -294,7 +303,7 @@ func (a *App) handleCommand(
})
if err := a.configService.SetConfig(newCfg); err != nil {
a.logger.Error("Remove destination failed", "err", err)
a.eventBus.Send(event.RemoveDestinationFailedEvent{Err: err})
a.eventBus.Send(event.RemoveDestinationFailedEvent{URL: c.URL, Err: err})
break
}
a.cfg = newCfg
@ -302,7 +311,7 @@ func (a *App) handleCommand(
a.eventBus.Send(event.DestinationRemovedEvent{URL: c.URL}) //nolint:gosimple
case event.CommandStartDestination:
if !state.Source.Live {
a.eventBus.Send(event.StartDestinationFailedEvent{})
a.eventBus.Send(event.StartDestinationFailedEvent{URL: c.URL, Message: "source not live"})
break
}
@ -315,7 +324,7 @@ func (a *App) handleCommand(
}
startMediaServerC <- struct{}{}
case event.CommandQuit:
case event.CommandKillServer:
return nil, errExit
}

View File

@ -1,4 +1,4 @@
package app
package server
import (
"testing"

View File

@ -3,6 +3,7 @@ package terminal
import (
"cmp"
"context"
"errors"
"fmt"
"log/slog"
"maps"
@ -44,9 +45,9 @@ type UI struct {
eventBus *event.Bus
dispatch func(event.Command)
clipboardAvailable bool
configFilePath string
rtmpURL, rtmpsURL string
buildInfo domain.BuildInfo
appExitC chan error
logger *slog.Logger
// tview state
@ -92,20 +93,20 @@ type ScreenCapture struct {
Width, Height int
}
// StartParams contains the parameters for starting a new terminal user
// Params contains the parameters for starting a new terminal user
// interface.
type StartParams struct {
type Params struct {
EventBus *event.Bus
Dispatcher func(event.Command)
Logger *slog.Logger
ClipboardAvailable bool
ConfigFilePath string
BuildInfo domain.BuildInfo
Screen *Screen // Screen may be nil.
}
// StartUI starts the terminal user interface.
func StartUI(ctx context.Context, params StartParams) (*UI, error) {
// NewUI creates the user interface. Call [Run] on the *UI instance to block
// until it is completed.
func NewUI(ctx context.Context, params Params) (*UI, error) {
app := tview.NewApplication()
var screen tcell.Screen
@ -211,7 +212,7 @@ func StartUI(ctx context.Context, params StartParams) (*UI, error) {
eventBus: params.EventBus,
dispatch: params.Dispatcher,
clipboardAvailable: params.ClipboardAvailable,
configFilePath: params.ConfigFilePath,
appExitC: make(chan error, 1),
buildInfo: params.BuildInfo,
logger: params.Logger,
app: app,
@ -237,8 +238,6 @@ func StartUI(ctx context.Context, params StartParams) (*UI, error) {
app.SetInputCapture(ui.inputCaptureHandler)
app.SetAfterDrawFunc(ui.afterDrawHandler)
go ui.run(ctx)
return ui, nil
}
@ -262,32 +261,32 @@ func (ui *UI) renderAboutView() {
ui.aboutView.AddItem(rtmpsURLView, 1, 0, false)
}
ui.aboutView.AddItem(tview.NewTextView().SetDynamicColors(true).SetText("[grey]c[-] Copy config file path"), 1, 0, false)
ui.aboutView.AddItem(tview.NewTextView().SetDynamicColors(true).SetText("[grey]?[-] About"), 1, 0, false)
}
func (ui *UI) run(ctx context.Context) {
defer func() {
// Ensure the application is stopped when the UI is closed.
ui.dispatch(event.CommandQuit{})
}()
var ErrUserClosed = errors.New("user closed UI")
// Run runs the user interface. It always returns a non-nil error, which will
// be [ErrUserClosed] if the user voluntarily closed the UI.
func (ui *UI) Run(ctx context.Context) error {
eventC := ui.eventBus.Register()
defer ui.eventBus.Deregister(eventC)
uiDone := make(chan struct{})
go func() {
defer func() {
uiDone <- struct{}{}
}()
if err := ui.app.Run(); err != nil {
ui.logger.Error("tui application error", "err", err)
err := ui.app.Run()
if err != nil {
ui.logger.Error("Error in UI run loop, exiting", "err", err)
}
ui.appExitC <- err
}()
for {
select {
case evt := <-eventC:
case evt, ok := <-eventC:
if !ok {
// should never happen
return errors.New("event channel closed")
}
ui.app.QueueUpdateDraw(func() {
switch evt := evt.(type) {
case event.AppStateChangedEvent:
@ -313,12 +312,11 @@ func (ui *UI) run(ctx context.Context) {
default:
ui.logger.Warn("unhandled event", "event", evt)
}
})
case <-ctx.Done():
return
case <-uiDone:
return
return ctx.Err()
case err := <-ui.appExitC:
return cmp.Or(err, ErrUserClosed)
}
}
}
@ -358,8 +356,6 @@ func (ui *UI) inputCaptureHandler(event *tcell.EventKey) *tcell.EventKey {
return nil
case ' ':
ui.toggleDestination()
case 'c', 'C':
ui.copyConfigFilePathToClipboard(ui.clipboardAvailable, ui.configFilePath)
case '?':
ui.showAbout()
case 'k': // tview vim bindings
@ -420,7 +416,7 @@ func (ui *UI) handleOtherInstanceDetected(event.OtherInstanceDetectedEvent) {
if buttonIndex == 0 {
ui.dispatch(event.CommandCloseOtherInstance{})
} else {
ui.dispatch(event.CommandQuit{})
ui.dispatch(event.CommandKillServer{})
}
},
)
@ -450,7 +446,7 @@ func (ui *UI) handleFatalErrorOccurred(evt event.FatalErrorOccurredEvent) {
[]string{"Quit"},
false,
func(int, string) {
ui.dispatch(event.CommandQuit{})
ui.dispatch(event.CommandKillServer{})
},
)
}
@ -825,6 +821,11 @@ func (ui *UI) Close() {
ui.app.Stop()
}
// Wait waits for the terminal user interface to finish.
func (ui *UI) Wait() {
<-ui.appExitC
}
func (ui *UI) addDestination() {
const (
inputLen = 60
@ -1006,28 +1007,6 @@ func (ui *UI) copySourceURLToClipboard(url string) {
)
}
func (ui *UI) copyConfigFilePathToClipboard(clipboardAvailable bool, configFilePath string) {
var text string
if clipboardAvailable {
if configFilePath != "" {
clipboard.Write(clipboard.FmtText, []byte(configFilePath))
text = "Configuration file path copied to clipboard:\n\n" + configFilePath
} else {
text = "Configuration file path not set"
}
} else {
text = "Copy to clipboard not available"
}
ui.showModal(
pageNameModalClipboard,
text,
[]string{"Ok"},
false,
nil,
)
}
func (ui *UI) confirmQuit() {
ui.showModal(
pageNameModalQuit,
@ -1036,7 +1015,7 @@ func (ui *UI) confirmQuit() {
false,
func(buttonIndex int, _ string) {
if buttonIndex == 0 {
ui.dispatch(event.CommandQuit{})
ui.app.Stop()
}
},
)

330
main.go
View File

@ -4,21 +4,23 @@ import (
"cmp"
"context"
"errors"
"flag"
"fmt"
"io"
"log/slog"
"os"
"os/exec"
"os/signal"
"runtime"
"runtime/debug"
"syscall"
"git.netflux.io/rob/octoplex/internal/app"
"git.netflux.io/rob/octoplex/internal/client"
"git.netflux.io/rob/octoplex/internal/config"
"git.netflux.io/rob/octoplex/internal/domain"
"git.netflux.io/rob/octoplex/internal/server"
dockerclient "github.com/docker/docker/client"
"github.com/urfave/cli/v2"
"golang.design/x/clipboard"
"golang.org/x/sync/errgroup"
)
var (
@ -30,23 +32,108 @@ var (
date string
)
var errShutdown = errors.New("shutdown")
// errInterrupt is an error type that indicates an interrupt signal was
// received.
type errInterrupt struct{}
func main() {
var exitStatus int
if err := run(); errors.Is(err, errShutdown) {
exitStatus = 130
} else if err != nil {
exitStatus = 1
_, _ = os.Stderr.WriteString("Error: " + err.Error() + "\n")
}
os.Exit(exitStatus)
// Error implements the error interface.
func (e errInterrupt) Error() string {
return "interrupt signal received"
}
func run() error {
ctx, cancel := context.WithCancelCause(context.Background())
// ExitCode implements the ExitCoder interface.
func (e errInterrupt) ExitCode() int {
return 130
}
func main() {
app := &cli.App{
Name: "Octoplex",
Usage: "Octoplex is a live video restreamer for Docker.",
Commands: []*cli.Command{
{
Name: "client",
Usage: "Run the client",
Action: func(c *cli.Context) error {
return runClient(c.Context, c)
},
},
{
Name: "server",
Usage: "Run the server",
Action: func(c *cli.Context) error {
return runServer(c.Context, c, serverConfig{
stderrAvailable: true,
handleSigInt: true,
waitForClient: false,
})
},
},
{
Name: "run",
Usage: "Run server and client together (testing)",
Action: func(c *cli.Context) error {
return runClientAndServer(c)
},
},
},
}
if err := app.Run(os.Args); err != nil {
fmt.Fprintf(os.Stderr, "Error: %v\n", err)
os.Exit(1)
}
}
func runClient(ctx context.Context, _ *cli.Context) error {
ctx, cancel := context.WithCancel(ctx)
defer cancel()
// TODO: logger from config
fptr, err := os.OpenFile("octoplex.log", os.O_RDWR|os.O_CREATE|os.O_APPEND, 0666)
if err != nil {
return fmt.Errorf("open log file: %w", err)
}
logger := slog.New(slog.NewTextHandler(fptr, nil))
logger.Info("Starting client", "version", cmp.Or(version, "devel"), "commit", cmp.Or(commit, "unknown"), "date", cmp.Or(date, "unknown"), "go_version", runtime.Version())
var clipboardAvailable bool
if err = clipboard.Init(); err != nil {
logger.Warn("Clipboard not available", "err", err)
} else {
clipboardAvailable = true
}
buildInfo, ok := debug.ReadBuildInfo()
if !ok {
return fmt.Errorf("read build info: %w", err)
}
app := client.New(client.NewParams{
ClipboardAvailable: clipboardAvailable,
BuildInfo: domain.BuildInfo{
GoVersion: buildInfo.GoVersion,
Version: version,
Commit: commit,
Date: date,
},
Logger: logger,
})
if err := app.Run(ctx); err != nil {
return fmt.Errorf("run app: %w", err)
}
return nil
}
type serverConfig struct {
stderrAvailable bool
handleSigInt bool
waitForClient bool
}
func runServer(ctx context.Context, _ *cli.Context, serverCfg serverConfig) error {
ctx, cancel := context.WithCancelCause(ctx)
defer cancel(nil)
configService, err := config.NewDefaultService()
@ -54,45 +141,35 @@ func run() error {
return fmt.Errorf("build config service: %w", err)
}
help := flag.Bool("h", false, "Show help")
flag.Parse()
if *help {
printUsage()
return nil
}
if narg := flag.NArg(); narg > 1 {
printUsage()
return fmt.Errorf("too many arguments")
} else if narg == 1 {
switch flag.Arg(0) {
case "edit-config":
return editConfigFile(configService)
case "print-config":
return printConfigPath(configService.Path())
case "version":
return printVersion()
case "help":
printUsage()
return nil
}
}
cfg, err := configService.ReadOrCreateConfig()
if err != nil {
return fmt.Errorf("read or create config: %w", err)
}
headless := os.Getenv("OCTO_HEADLESS") != ""
logger, err := buildLogger(cfg.LogFile, headless)
if err != nil {
return fmt.Errorf("build logger: %w", err)
// TODO: improve logger API
// Currently it's a bit complicated because we can only use stdout - the
// preferred destination - if the client is not running. Otherwise we
// fallback to the legacy configuration but this should be bought more
// in-line with the client/server split.
var w io.Writer
if serverCfg.stderrAvailable {
w = os.Stdout
} else if !cfg.LogFile.Enabled {
w = io.Discard
} else {
w, err = os.OpenFile(cfg.LogFile.GetPath(), os.O_RDWR|os.O_CREATE|os.O_APPEND, 0666)
if err != nil {
return fmt.Errorf("error opening log file: %w", err)
}
}
if headless {
// When running in headless mode tview doesn't handle SIGINT for us.
var handlerOpts slog.HandlerOptions
if os.Getenv("OCTO_DEBUG") != "" {
handlerOpts.Level = slog.LevelDebug
}
logger := slog.New(slog.NewTextHandler(w, &handlerOpts))
if serverCfg.handleSigInt {
ch := make(chan os.Signal, 1)
signal.Notify(ch, syscall.SIGINT, syscall.SIGTERM)
@ -100,17 +177,10 @@ func run() error {
<-ch
logger.Info("Received interrupt signal, exiting")
signal.Stop(ch)
cancel(errShutdown)
cancel(errInterrupt{})
}()
}
var clipboardAvailable bool
if err = clipboard.Init(); err != nil {
logger.Warn("Clipboard not available", "err", err)
} else {
clipboardAvailable = true
}
dockerClient, err := dockerclient.NewClientWithOpts(
dockerclient.FromEnv,
dockerclient.WithAPIVersionNegotiation(),
@ -119,102 +189,64 @@ func run() error {
return fmt.Errorf("new docker client: %w", err)
}
buildInfo, ok := debug.ReadBuildInfo()
if !ok {
return fmt.Errorf("read build info: %w", err)
}
app := app.New(app.Params{
ConfigService: configService,
DockerClient: dockerClient,
Headless: headless,
ClipboardAvailable: clipboardAvailable,
ConfigFilePath: configService.Path(),
BuildInfo: domain.BuildInfo{
GoVersion: buildInfo.GoVersion,
Version: version,
Commit: commit,
Date: date,
},
Logger: logger,
app := server.New(server.Params{
ConfigService: configService,
DockerClient: dockerClient,
ConfigFilePath: configService.Path(),
WaitForClient: serverCfg.waitForClient,
Logger: logger,
})
return app.Run(ctx)
}
logger.Info(
"Starting server",
"version",
cmp.Or(version, "devel"),
"commit",
cmp.Or(commit, "unknown"),
"date",
cmp.Or(date, "unknown"),
"go_version",
runtime.Version(),
)
// editConfigFile opens the config file in the user's editor.
func editConfigFile(configService *config.Service) error {
if _, err := configService.ReadOrCreateConfig(); err != nil {
return fmt.Errorf("read or create config: %w", err)
}
editor := os.Getenv("EDITOR")
if editor == "" {
editor = "vi"
}
binary, err := exec.LookPath(editor)
if err != nil {
return fmt.Errorf("look path: %w", err)
}
fmt.Fprintf(os.Stderr, "Editing config file: %s\n", configService.Path())
fmt.Println(binary)
if err := syscall.Exec(binary, []string{"--", configService.Path()}, os.Environ()); err != nil {
return fmt.Errorf("exec: %w", err)
}
return nil
}
// printConfigPath prints the path to the config file to stderr.
func printConfigPath(configPath string) error {
fmt.Fprintln(os.Stderr, configPath)
return nil
}
// printVersion prints the version of the application to stderr.
func printVersion() error {
fmt.Fprintf(os.Stderr, "%s version %s\n", domain.AppName, cmp.Or(version, "0.0.0-dev"))
return nil
}
func printUsage() {
os.Stderr.WriteString("Usage: octoplex [command]\n\n")
os.Stderr.WriteString("Commands:\n\n")
os.Stderr.WriteString(" edit-config Edit the config file\n")
os.Stderr.WriteString(" print-config Print the path to the config file\n")
os.Stderr.WriteString(" version Print the version of the application\n")
os.Stderr.WriteString(" help Print this help message\n")
os.Stderr.WriteString("\n")
os.Stderr.WriteString("Additionally, Octoplex can be configured with the following environment variables:\n\n")
os.Stderr.WriteString(" OCTO_DEBUG Enables debug logging if set\n")
os.Stderr.WriteString(" OCTO_HEADLESS Enables headless mode if set (experimental)\n\n")
}
// buildLogger builds the logger, which may be a no-op logger.
func buildLogger(cfg config.LogFile, headless bool) (*slog.Logger, error) {
build := func(w io.Writer) *slog.Logger {
var handlerOpts slog.HandlerOptions
if os.Getenv("OCTO_DEBUG") != "" {
handlerOpts.Level = slog.LevelDebug
if err := app.Run(ctx); err != nil {
if errors.Is(err, context.Canceled) && errors.Is(context.Cause(ctx), errInterrupt{}) {
return context.Cause(ctx)
}
return slog.New(slog.NewTextHandler(w, &handlerOpts))
return err
}
// In headless mode, always log to stderr.
if headless {
return build(os.Stderr), nil
}
if !cfg.Enabled {
return slog.New(slog.DiscardHandler), nil
}
fptr, err := os.OpenFile(cfg.GetPath(), os.O_RDWR|os.O_CREATE|os.O_APPEND, 0666)
if err != nil {
return nil, fmt.Errorf("error opening log file: %w", err)
}
return build(fptr), nil
return nil
}
func runClientAndServer(c *cli.Context) error {
errNoErr := errors.New("no error")
g, ctx := errgroup.WithContext(c.Context)
g.Go(func() error {
if err := runClient(ctx, c); err != nil {
return err
}
return errNoErr
})
g.Go(func() error {
if err := runServer(ctx, c, serverConfig{
stderrAvailable: false,
handleSigInt: false,
waitForClient: true,
}); err != nil {
return err
}
return errNoErr
})
if err := g.Wait(); err == errNoErr {
return nil
} else {
return err
}
}

View File

@ -40,3 +40,9 @@ description = "Generate mocks"
dir = "{{cwd}}"
run = "go tool mockery"
alias = "m"
[tasks.generate_proto]
description = "Generate gRPC files from proto"
dir = "{{cwd}}"
run = "protoc -I proto --go_out=paths=source_relative:internal/generated/grpc --go-grpc_out=paths=source_relative:internal/generated/grpc proto/api.proto proto/domain.proto proto/event.proto proto/command.proto"
alias = "p"

18
proto/api.proto Normal file
View File

@ -0,0 +1,18 @@
syntax = "proto3";
option go_package = "git.netflux.io/rob/octoplex/internal/generated/grpc";
package api;
import "event.proto";
import "command.proto";
service InternalAPI {
rpc Communicate(stream Envelope) returns (stream Envelope);
}
message Envelope {
oneof payload {
Command command = 1;
Event event = 2;
}
}

33
proto/command.proto Normal file
View File

@ -0,0 +1,33 @@
syntax = "proto3";
package api;
option go_package = "git.netflux.io/rob/octoplex/internal/generated/grpc";
message Command {
oneof command_type {
AddDestinationCommand add_destination = 1;
RemoveDestinationCommand remove_destination = 2;
StartDestinationCommand start_destination = 3;
StopDestinationCommand stop_destination = 4;
CloseOtherInstancesCommand close_other_instances = 5;
KillServerCommand kill_server = 6;
StartHandshakeCommand start_handshake = 7;
}
}
message AddDestinationCommand {
string name = 1;
string url = 2;
}
message RemoveDestinationCommand { string url = 1; }
message StartDestinationCommand { string url = 1; }
message StopDestinationCommand { string url = 1; }
message CloseOtherInstancesCommand {}
message KillServerCommand {}
message StartHandshakeCommand {};

58
proto/domain.proto Normal file
View File

@ -0,0 +1,58 @@
syntax = "proto3";
package api;
option go_package = "git.netflux.io/rob/octoplex/internal/generated/grpc";
import "google/protobuf/timestamp.proto";
message Container {
string id = 1;
string status = 2;
string health_state = 3;
double cpu_percent = 4;
uint64 memory_usage_bytes = 5;
int32 rx_rate = 6;
int32 tx_rate = 7;
google.protobuf.Timestamp rx_since = 8;
string image_name = 9;
string pull_status = 10;
string pull_progress = 11;
int32 pull_percent = 12;
int32 restart_count = 13;
optional int32 exit_code = 14;
string err = 15;
}
message Source {
Container container = 1;
bool live = 2;
google.protobuf.Timestamp live_changed_at = 3;
repeated string tracks = 4;
string exit_reason = 5;
}
message Destination {
enum Status {
STATUS_OFF_AIR = 0;
STATUS_STARTING = 1;
STATUS_LIVE = 2;
}
Container container = 1;
Status status = 2;
string name = 3;
string url = 4;
}
message BuildInfo {
string go_version = 1;
string version = 2;
string commit = 3;
string date = 4;
}
message AppState {
Source source = 1;
repeated Destination destinations = 2;
BuildInfo build_info = 3;
}

59
proto/event.proto Normal file
View File

@ -0,0 +1,59 @@
syntax = "proto3";
package api;
option go_package = "git.netflux.io/rob/octoplex/internal/generated/grpc";
import "domain.proto";
message Event {
oneof event_type {
AppStateChangedEvent app_state_changed = 1;
DestinationStreamExitedEvent destination_stream_exited = 2;
DestinationAddedEvent destination_added = 3;
AddDestinationFailedEvent add_destination_failed = 4;
DestinationRemovedEvent destination_removed = 5;
RemoveDestinationFailedEvent remove_destination_failed = 6;
StartDestinationFailedEvent start_destination_failed = 7;
MediaServerStartedEvent media_server_started = 8;
OtherInstanceDetectedEvent other_instance_detected = 9;
FatalErrorEvent fatal_error = 10;
HandshakeCompletedEvent handshake_completed = 11;
}
}
message AppStateChangedEvent { AppState app_state = 1; }
message DestinationStreamExitedEvent {
string name = 1;
string error = 2;
}
message DestinationAddedEvent { string url = 1; }
message AddDestinationFailedEvent {
string url = 1;
string error = 2;
}
message DestinationRemovedEvent { string url = 1; }
message RemoveDestinationFailedEvent {
string url = 1;
string error = 2;
}
message StartDestinationFailedEvent {
string url = 1;
string message = 2;
}
message MediaServerStartedEvent {
string rtmp_url = 1;
string rtmps_url = 2;
}
message OtherInstanceDetectedEvent {}
message FatalErrorEvent { string message = 1; }
message HandshakeCompletedEvent {}