Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

WIP: Add external dashboard daemon #1

Closed
wants to merge 5 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 7 additions & 7 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -19,13 +19,13 @@ endif
project = heminetwork
version = $(shell git describe --tags 2>/dev/null || echo "v0.0.0")

cmds = \
bfgd \
bssd \
extool \
keygen \
popmd \
hemictl
cmds = bfgd \
bssd \
dashd \
extool \
hemictl \
keygen \
popmd

.PHONY: all clean clean-dist deps $(cmds) build install lint lint-deps tidy race test vulncheck \
vulncheck-deps dist archive sources checksums networktest
Expand Down
107 changes: 107 additions & 0 deletions api/dashapi/dashapi.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
// Copyright (c) 2024 Hemi Labs, Inc.
// Use of this source code is governed by the MIT License,
// which can be found in the LICENSE file.

package dashapi

import (
"context"
"fmt"
"reflect"

"github.com/hemilabs/heminetwork/api/protocol"
)

const (
APIVersion = 1
)

var (
APIVersionRoute = fmt.Sprintf("v%d", APIVersion)
RouteWebsocket = fmt.Sprintf("/%s/ws", APIVersionRoute)
DefaultListen = "localhost:49153"
DefaultPrometheusListen = "localhost:2112"
DefaultURL = "ws://" + DefaultListen + RouteWebsocket
)

const (
// Generic RPC commands
CmdPingRequest = "dashapi-ping-request"
CmdPingResponse = "dashapi-ping-response"

// Custom RPC commands
CmdHeartbeatRequest protocol.Command = "dashapi-heartbeat-request"
CmdHeartbeatResponse protocol.Command = "dashapi-heartbeat-response"
)

type (
PingRequest protocol.PingRequest
PingResponse protocol.PingResponse
)

type HeartbeatRequest struct {
Timestamp int64 `json:"timestamp"`
}

type HeartbeatResponse struct {
Error *protocol.Error `json:"error,omitempty"`
}

// commands contains the command key and type. This is used during RPC calls.
var commands = map[protocol.Command]reflect.Type{
CmdPingRequest: reflect.TypeOf(PingRequest{}),
CmdPingResponse: reflect.TypeOf(PingResponse{}),
CmdHeartbeatRequest: reflect.TypeOf(HeartbeatRequest{}),
CmdHeartbeatResponse: reflect.TypeOf(HeartbeatResponse{}),
}

// apiCmd is an empty structure used to satisfy the protocol.API interface.
type apiCmd struct{}

// Commands satisfies the protocol.API interface.
func (a *apiCmd) Commands() map[protocol.Command]reflect.Type {
return commands
}

func APICommands() map[protocol.Command]reflect.Type {
return commands // XXX make copy
}

// Error is the dash protocol error type
type Error protocol.Error

func (e Error) String() string {
return (protocol.Error)(e).String()
}

func Errorf(msg string, args ...interface{}) *Error {
return (*Error)(protocol.Errorf(msg, args...))
}

// Read reads a command from an APIConn. This is used server side.
func Read(ctx context.Context, c protocol.APIConn) (protocol.Command, string, any, error) {
return protocol.Read(ctx, c, &apiCmd{})
}

// Write writes a command to an APIConn. This is used server side.
func Write(ctx context.Context, c protocol.APIConn, id string, payload any) error {
return protocol.Write(ctx, c, &apiCmd{}, id, payload)
}

// Call executes a blocking RPC call. Note that this requires the client to
// provide a ReadConn in a for loop in order to receive commands. This may be
// fixed in the future but seems simple enough to just leave alone for now. The
// need for the ReadConn loop is because apiCmd is not exported.
func Call(ctx context.Context, c *protocol.Conn, payload any) (protocol.Command, string, any, error) {
return c.Call(ctx, &apiCmd{}, payload)
}

// ReadConn reads a command from a protocol.Conn. This is used client side.
func ReadConn(ctx context.Context, c *protocol.Conn) (protocol.Command, string, any, error) {
return c.Read(ctx, &apiCmd{})
}

// WriteConn writes a command to a protocol.Conn. This is used client side.
func WriteConn(ctx context.Context, c *protocol.Conn, id string, payload any) error {
return c.Write(ctx, &apiCmd{}, id, payload)
}
65 changes: 59 additions & 6 deletions api/protocol/protocol.go
Original file line number Diff line number Diff line change
Expand Up @@ -231,16 +231,15 @@ type Message struct {
}

// Error is a protocol Error type that can be used for additional error
// context. It embeds an 8 byte number that can be used to trace calls on both the
// client and server side.
// context. It embeds an 8 byte number that can be used to trace calls on both
// the client and server side.
type Error struct {
Timestamp int64 `json:"timestamp"`
Trace string `json:"trace"`
Message string `json:"error"`
Trace string `json:"trace,omitempty"`
Message string `json:"message"`
}

// Errorf is a client induced protocol error (e.g. "invalid height"). This is a
// pretty printable error on the client and server and is not fatal.
// Errorf returns a protocol Error type with an embedded trace.
func Errorf(msg string, args ...interface{}) *Error {
trace, _ := random(8)
return &Error{
Expand All @@ -250,10 +249,64 @@ func Errorf(msg string, args ...interface{}) *Error {
}
}

// String pretty prints a protocol error.
func (e Error) String() string {
if len(e.Trace) == 0 {
return e.Message
}
return fmt.Sprintf("%v [%v:%v]", e.Message, e.Trace, e.Timestamp)
}

// WireError converts an application error to a protocol Error. This does not
// embed a trace since the error is essentially pass through and therefore won't
// be logged server side.
func WireError(err error) *Error {
return &Error{
Timestamp: time.Now().Unix(),
Message: err.Error(),
}
}

// InternalError is an error type that differentiates between caller and callee
// errors. An internal error is used when something internal to the application
// fails. The client should not see the actual error message as those are
// server operator specific.
//
// One can argue that this not belong here but to prevent heavy copy/paste that
// will not age well it has been moved here.
type InternalError struct {
internal *Error
actual error
}

// WireError returns the protocol error representation.
func (ie InternalError) WireError() *Error {
return ie.internal
}

// Error satisfies the error interface.
func (ie InternalError) Error() string {
if ie.actual != nil {
return fmt.Sprintf("%v [%v:%v]", ie.actual.Error(),
ie.internal.Timestamp, ie.internal.Trace)
}
return ie.internal.String()
}

// NewInternalErrorf returns an InternalError constructed from the passed
// message and arguments.
func NewInternalErrorf(msg string, args ...interface{}) *InternalError {
return &InternalError{
internal: Errorf("internal error"),
actual: fmt.Errorf(msg, args...),
}
}

// NewInternalError returns an InternalError representation of the passed in error.
func NewInternalError(err error) *InternalError {
return NewInternalErrorf("internal error: %v", err)
}

// Ping
type PingRequest struct {
Timestamp int64 `json:"timestamp"` // Local timestamp
Expand Down
117 changes: 117 additions & 0 deletions cmd/dashd/dashd.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
// Copyright (c) 2024 Hemi Labs, Inc.
// Use of this source code is governed by the MIT License,
// which can be found in the LICENSE file.

package main

import (
"context"
"fmt"
"os"
"os/signal"

"github.com/hemilabs/heminetwork/api/dashapi"
"github.com/hemilabs/heminetwork/config"
"github.com/hemilabs/heminetwork/service/dash"
"github.com/hemilabs/heminetwork/version"
"github.com/juju/loggo"
)

const (
daemonName = "dashd"
defaultLogLevel = daemonName + "=INFO:protocol=INFO:dash=INFO"
)

var (
log = loggo.GetLogger(daemonName)
welcome = fmt.Sprintf("Hemi Dashboard Daemon: v%s", version.String())

cfg = dash.NewDefaultConfig()
cm = config.CfgMap{
"DASH_ADDRESS": config.Config{
Value: &cfg.ListenAddress,
DefaultValue: dashapi.DefaultListen,
Help: "address and port dashd listens on",
Print: config.PrintAll,
},
"DASH_LOG_LEVEL": config.Config{
Value: &cfg.LogLevel,
DefaultValue: defaultLogLevel,
Help: "loglevel for various packages; INFO, DEBUG and TRACE",
Print: config.PrintAll,
},
"DASH_PROMETHEUS_ADDRESS": config.Config{
Value: &cfg.PrometheusListenAddress,
DefaultValue: "",
Help: "address and port dashd prometheus listens on",
Print: config.PrintAll,
},
}
)

func HandleSignals(ctx context.Context, cancel context.CancelFunc, callback func(os.Signal)) {
signalChan := make(chan os.Signal, 1)
signal.Notify(signalChan, os.Interrupt)
signal.Notify(signalChan, os.Kill)
defer func() {
signal.Stop(signalChan)
cancel()
}()

select {
case <-ctx.Done():
case s := <-signalChan: // First signal, cancel context.
if callback != nil {
callback(s) // Do whatever caller wants first.
cancel()
}
}
<-signalChan // Second signal, hard exit.
os.Exit(2)
}

func _main() error {
// Parse configuration from environment
if err := config.Parse(cm); err != nil {
return err
}

loggo.ConfigureLoggers(cfg.LogLevel)
log.Infof("%v", welcome)

pc := config.PrintableConfig(cm)
for k := range pc {
log.Infof("%v", pc[k])
}

ctx, cancel := context.WithCancel(context.Background())
go HandleSignals(ctx, cancel, func(s os.Signal) {
log.Infof("dash service received signal: %s", s)
})

server, err := dash.NewServer(cfg)
if err != nil {
return fmt.Errorf("Failed to create BSS server: %v", err)
}
if err := server.Run(ctx); err != context.Canceled {
return fmt.Errorf("BSS server terminated with error: %v", err)
}

return nil
}

func main() {
if len(os.Args) != 1 {
fmt.Fprintf(os.Stderr, "%v\n", welcome)
fmt.Fprintf(os.Stderr, "Usage:\n")
fmt.Fprintf(os.Stderr, "\thelp (this help)\n")
fmt.Fprintf(os.Stderr, "Environment:\n")
config.Help(os.Stderr, cm)
os.Exit(1)
}

if err := _main(); err != nil {
fmt.Fprintf(os.Stderr, "%v\n", err)
os.Exit(1)
}
}
18 changes: 18 additions & 0 deletions cmd/hemictl/hemictl.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ import (

"github.com/hemilabs/heminetwork/api/bfgapi"
"github.com/hemilabs/heminetwork/api/bssapi"
"github.com/hemilabs/heminetwork/api/dashapi"
"github.com/hemilabs/heminetwork/api/protocol"
"github.com/hemilabs/heminetwork/config"
"github.com/hemilabs/heminetwork/database/bfgd/postgres"
Expand Down Expand Up @@ -84,6 +85,16 @@ func handleBFGWebsocketReadUnauth(ctx context.Context, conn *protocol.Conn) {
}
}

// handleBSSWebsocketReadUnauth discards all reads but has to exist in order to
// be able to use dashapi.Call.
func handleDashWebsocketReadUnauth(ctx context.Context, conn *protocol.Conn) {
for {
if _, _, _, err := dashapi.ReadConn(ctx, conn); err != nil {
return
}
}
}

func bfgdb() error {
ctx, cancel := context.WithTimeout(context.Background(), callTimeout)
defer cancel()
Expand Down Expand Up @@ -304,6 +315,9 @@ func init() {
for k, v := range bfgapi.APICommands() {
allCommands[string(k)] = v
}
for k, v := range dashapi.APICommands() {
allCommands[string(k)] = v
}

sortedCommands = make([]string, 0, len(allCommands))
for k := range allCommands {
Expand Down Expand Up @@ -410,6 +424,10 @@ func _main() error {
u = bfgapi.DefaultPrivateURL
callHandler = handleBFGWebsocketReadUnauth
call = bfgapi.Call // XXX yuck
case strings.HasPrefix(cmd, "dashapi"):
u = dashapi.DefaultURL
callHandler = handleDashWebsocketReadUnauth
call = dashapi.Call // XXX yuck
default:
return fmt.Errorf("can't derive URL from command: %v", cmd)
}
Expand Down
4 changes: 2 additions & 2 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ require (
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.2.0
github.com/docker/docker v25.0.3+incompatible
github.com/docker/go-connections v0.5.0
github.com/ethereum/go-ethereum v1.13.5
github.com/ethereum/go-ethereum v1.13.13
github.com/go-test/deep v1.1.0
github.com/juju/loggo v1.0.0
github.com/lib/pq v1.10.9
Expand Down Expand Up @@ -58,7 +58,7 @@ require (
github.com/pkg/errors v0.9.1 // indirect
github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55 // indirect
github.com/prometheus/client_model v0.6.0 // indirect
github.com/prometheus/common v0.47.0 // indirect
github.com/prometheus/common v0.48.0 // indirect
github.com/prometheus/procfs v0.12.0 // indirect
github.com/shirou/gopsutil/v3 v3.24.1 // indirect
github.com/shoenig/go-m1cpu v0.1.6 // indirect
Expand Down
Loading
Loading