mirror of
https://github.com/datarhei/core.git
synced 2025-10-06 00:17:07 +08:00

Replace CORE_CLUSTER_JOIN_ADDRESS with CORE_CLUSTER_PEERS. This is a comma separated list of cluster members with their IDs of the form ID@host:port On startup the node tries to connect to all the peers. In case of sudden deaths of a node this will allow to find back into the cluster. The list in CLUSTER_PEERS is a starting point of known peers. Other node that are not in that list can still join the cluster. File and stream proxy has been moved to the Proxy type.
369 lines
6.5 KiB
Go
369 lines
6.5 KiB
Go
package cluster
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"io"
|
|
"net"
|
|
"net/http"
|
|
"net/url"
|
|
"path/filepath"
|
|
"regexp"
|
|
"strings"
|
|
"sync"
|
|
"time"
|
|
|
|
"github.com/datarhei/core/v16/client"
|
|
)
|
|
|
|
type NodeReader interface {
|
|
Address() string
|
|
IPs() []string
|
|
State() NodeState
|
|
}
|
|
|
|
type NodeState struct {
|
|
ID string
|
|
State string
|
|
Files []string
|
|
LastUpdate time.Time
|
|
}
|
|
|
|
type NodeSpecs struct {
|
|
ID string
|
|
Address string
|
|
}
|
|
|
|
type nodeState string
|
|
|
|
func (n nodeState) String() string {
|
|
return string(n)
|
|
}
|
|
|
|
const (
|
|
stateDisconnected nodeState = "disconnected"
|
|
stateConnected nodeState = "connected"
|
|
)
|
|
|
|
type node struct {
|
|
address string
|
|
ips []string
|
|
state nodeState
|
|
username string
|
|
password string
|
|
updates chan<- NodeState
|
|
peer client.RestClient
|
|
filesList []string
|
|
lastUpdate time.Time
|
|
lock sync.RWMutex
|
|
cancel context.CancelFunc
|
|
once sync.Once
|
|
|
|
host string
|
|
secure bool
|
|
hasRTMP bool
|
|
rtmpAddress string
|
|
rtmpToken string
|
|
hasSRT bool
|
|
srtAddress string
|
|
srtPassphrase string
|
|
srtToken string
|
|
|
|
prefix *regexp.Regexp
|
|
}
|
|
|
|
func newNode(address string, updates chan<- NodeState) (*node, error) {
|
|
u, err := url.Parse(address)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("invalid address: %w", err)
|
|
}
|
|
|
|
host, _, err := net.SplitHostPort(u.Host)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("invalid address: %w", err)
|
|
}
|
|
|
|
addrs, err := net.LookupHost(host)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("lookup failed: %w", err)
|
|
}
|
|
|
|
username := u.User.Username()
|
|
password := ""
|
|
if pw, ok := u.User.Password(); ok {
|
|
password = pw
|
|
}
|
|
|
|
n := &node{
|
|
address: address,
|
|
ips: addrs,
|
|
username: username,
|
|
password: password,
|
|
state: stateDisconnected,
|
|
updates: updates,
|
|
prefix: regexp.MustCompile(`^[a-z]+:`),
|
|
host: host,
|
|
secure: strings.HasPrefix(address, "https://"),
|
|
}
|
|
|
|
peer, err := client.New(client.Config{
|
|
Address: address,
|
|
Username: username,
|
|
Password: password,
|
|
Auth0Token: "",
|
|
Client: &http.Client{
|
|
Timeout: 5 * time.Second,
|
|
},
|
|
})
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
config, err := peer.Config()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
if config.Config.RTMP.Enable {
|
|
n.hasRTMP = true
|
|
n.rtmpAddress = "rtmp://"
|
|
|
|
isHostIP := net.ParseIP(host) != nil
|
|
|
|
address := config.Config.RTMP.Address
|
|
if n.secure && config.Config.RTMP.EnableTLS && !isHostIP {
|
|
address = config.Config.RTMP.AddressTLS
|
|
n.rtmpAddress = "rtmps://"
|
|
}
|
|
|
|
_, port, err := net.SplitHostPort(address)
|
|
if err != nil {
|
|
n.hasRTMP = false
|
|
} else {
|
|
n.rtmpAddress += host + ":" + port
|
|
n.rtmpToken = config.Config.RTMP.Token
|
|
}
|
|
}
|
|
|
|
if config.Config.SRT.Enable {
|
|
n.hasSRT = true
|
|
n.srtAddress = "srt://"
|
|
|
|
_, port, err := net.SplitHostPort(config.Config.SRT.Address)
|
|
if err != nil {
|
|
n.hasSRT = false
|
|
} else {
|
|
n.srtAddress += host + ":" + port
|
|
n.srtPassphrase = config.Config.SRT.Passphrase
|
|
n.srtToken = config.Config.SRT.Token
|
|
}
|
|
}
|
|
|
|
n.peer = peer
|
|
|
|
ctx, cancel := context.WithCancel(context.Background())
|
|
n.cancel = cancel
|
|
|
|
go func(ctx context.Context) {
|
|
ticker := time.NewTicker(time.Second)
|
|
defer ticker.Stop()
|
|
|
|
for {
|
|
select {
|
|
case <-ctx.Done():
|
|
return
|
|
case <-ticker.C:
|
|
n.files()
|
|
|
|
select {
|
|
case n.updates <- n.State():
|
|
default:
|
|
}
|
|
}
|
|
}
|
|
}(ctx)
|
|
|
|
return n, nil
|
|
}
|
|
|
|
func (n *node) Address() string {
|
|
return n.address
|
|
}
|
|
|
|
func (n *node) IPs() []string {
|
|
return n.ips
|
|
}
|
|
|
|
func (n *node) ID() string {
|
|
return n.peer.ID()
|
|
}
|
|
|
|
func (n *node) State() NodeState {
|
|
n.lock.RLock()
|
|
defer n.lock.RUnlock()
|
|
|
|
state := NodeState{
|
|
ID: n.peer.ID(),
|
|
LastUpdate: n.lastUpdate,
|
|
}
|
|
|
|
if n.state == stateDisconnected || time.Since(n.lastUpdate) > 2*time.Second {
|
|
state.State = stateDisconnected.String()
|
|
} else {
|
|
state.State = n.state.String()
|
|
state.Files = make([]string, len(n.filesList))
|
|
copy(state.Files, n.filesList)
|
|
}
|
|
|
|
return state
|
|
}
|
|
|
|
func (n *node) stop() {
|
|
n.once.Do(func() { n.cancel() })
|
|
}
|
|
|
|
func (n *node) files() {
|
|
filesChan := make(chan string, 1024)
|
|
filesList := []string{}
|
|
|
|
wgList := sync.WaitGroup{}
|
|
wgList.Add(1)
|
|
|
|
go func() {
|
|
defer wgList.Done()
|
|
|
|
for file := range filesChan {
|
|
if len(file) == 0 {
|
|
return
|
|
}
|
|
|
|
filesList = append(filesList, file)
|
|
}
|
|
}()
|
|
|
|
wg := sync.WaitGroup{}
|
|
wg.Add(2)
|
|
|
|
go func(f chan<- string) {
|
|
defer wg.Done()
|
|
|
|
files, err := n.peer.MemFSList("name", "asc")
|
|
if err != nil {
|
|
return
|
|
}
|
|
|
|
for _, file := range files {
|
|
f <- "mem:" + file.Name
|
|
}
|
|
}(filesChan)
|
|
|
|
go func(f chan<- string) {
|
|
defer wg.Done()
|
|
|
|
files, err := n.peer.DiskFSList("name", "asc")
|
|
if err != nil {
|
|
return
|
|
}
|
|
|
|
for _, file := range files {
|
|
f <- "disk:" + file.Name
|
|
}
|
|
}(filesChan)
|
|
|
|
if n.hasRTMP {
|
|
wg.Add(1)
|
|
|
|
go func(f chan<- string) {
|
|
defer wg.Done()
|
|
|
|
files, err := n.peer.RTMPChannels()
|
|
if err != nil {
|
|
return
|
|
}
|
|
|
|
for _, file := range files {
|
|
f <- "rtmp:" + file.Name
|
|
}
|
|
}(filesChan)
|
|
}
|
|
|
|
if n.hasSRT {
|
|
wg.Add(1)
|
|
|
|
go func(f chan<- string) {
|
|
defer wg.Done()
|
|
|
|
files, err := n.peer.SRTChannels()
|
|
if err != nil {
|
|
return
|
|
}
|
|
|
|
for _, file := range files {
|
|
f <- "srt:" + file.Name
|
|
}
|
|
}(filesChan)
|
|
}
|
|
|
|
wg.Wait()
|
|
|
|
filesChan <- ""
|
|
|
|
wgList.Wait()
|
|
|
|
n.lock.Lock()
|
|
|
|
n.filesList = make([]string, len(filesList))
|
|
copy(n.filesList, filesList)
|
|
n.lastUpdate = time.Now()
|
|
n.state = stateConnected
|
|
|
|
n.lock.Unlock()
|
|
}
|
|
|
|
func (n *node) getURL(path string) (string, error) {
|
|
// Remove prefix from path
|
|
prefix := n.prefix.FindString(path)
|
|
path = n.prefix.ReplaceAllString(path, "")
|
|
|
|
u := ""
|
|
|
|
if prefix == "mem:" {
|
|
u = n.address + "/" + filepath.Join("memfs", path)
|
|
} else if prefix == "disk:" {
|
|
u = n.address + path
|
|
} else if prefix == "rtmp:" {
|
|
u = n.rtmpAddress + path
|
|
if len(n.rtmpToken) != 0 {
|
|
u += "?token=" + url.QueryEscape(n.rtmpToken)
|
|
}
|
|
} else if prefix == "srt:" {
|
|
u = n.srtAddress + "?mode=caller"
|
|
if len(n.srtPassphrase) != 0 {
|
|
u += "&passphrase=" + url.QueryEscape(n.srtPassphrase)
|
|
}
|
|
streamid := "#!:m=request,r=" + path
|
|
if len(n.srtToken) != 0 {
|
|
streamid += ",token=" + n.srtToken
|
|
}
|
|
u += "&streamid=" + url.QueryEscape(streamid)
|
|
} else {
|
|
return "", fmt.Errorf("unknown prefix")
|
|
}
|
|
|
|
return u, nil
|
|
}
|
|
|
|
func (n *node) getFile(path string) (io.ReadCloser, error) {
|
|
// Remove prefix from path
|
|
prefix := n.prefix.FindString(path)
|
|
path = n.prefix.ReplaceAllString(path, "")
|
|
|
|
if prefix == "mem:" {
|
|
return n.peer.MemFSGetFile(path)
|
|
} else if prefix == "disk:" {
|
|
return n.peer.DiskFSGetFile(path)
|
|
}
|
|
|
|
return nil, fmt.Errorf("unknown prefix")
|
|
}
|