Refactor node storage and use proper types for protocol data
- Rename TypeID to ID throughout - Remove re-derivable data (MACTableSize, SACNInputs now derived) - Use typed ArtNetUniverse and SACNUniverse with methods - Store multicast groups with lastSeen tracking in structs - Remove int indexes in Nodes, use direct node pointers - Parse multicast groups into typed struct instead of strings Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
72
artnet.go
72
artnet.go
@@ -182,53 +182,58 @@ func (n *Nodes) UpdateArtNet(node *Node, inputs, outputs []int) {
|
||||
n.mu.Lock()
|
||||
defer n.mu.Unlock()
|
||||
|
||||
for _, u := range inputs {
|
||||
if !containsInt(node.ArtNetInputs, u) {
|
||||
node.ArtNetInputs = append(node.ArtNetInputs, u)
|
||||
if node.ArtNetInputs == nil {
|
||||
node.ArtNetInputs = ArtNetUniverseSet{}
|
||||
}
|
||||
if node.ArtNetOutputs == nil {
|
||||
node.ArtNetOutputs = ArtNetUniverseSet{}
|
||||
}
|
||||
|
||||
for _, u := range inputs {
|
||||
node.ArtNetInputs.Add(ArtNetUniverse(u))
|
||||
}
|
||||
for _, u := range outputs {
|
||||
if !containsInt(node.ArtNetOutputs, u) {
|
||||
node.ArtNetOutputs = append(node.ArtNetOutputs, u)
|
||||
node.ArtNetOutputs.Add(ArtNetUniverse(u))
|
||||
}
|
||||
}
|
||||
sort.Ints(node.ArtNetInputs)
|
||||
sort.Ints(node.ArtNetOutputs)
|
||||
node.artnetLastSeen = time.Now()
|
||||
}
|
||||
|
||||
func (n *Nodes) expireArtNet() {
|
||||
expireTime := time.Now().Add(-60 * time.Second)
|
||||
for _, node := range n.nodes {
|
||||
if !node.artnetLastSeen.IsZero() && node.artnetLastSeen.Before(expireTime) {
|
||||
node.ArtNetInputs = nil
|
||||
node.ArtNetOutputs = nil
|
||||
node.artnetLastSeen = time.Time{}
|
||||
if node.ArtNetInputs != nil {
|
||||
node.ArtNetInputs.Expire(60 * time.Second)
|
||||
}
|
||||
if node.ArtNetOutputs != nil {
|
||||
node.ArtNetOutputs.Expire(60 * time.Second)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (n *Nodes) mergeArtNet(keep, merge *Node) {
|
||||
for _, u := range merge.ArtNetInputs {
|
||||
if !containsInt(keep.ArtNetInputs, u) {
|
||||
keep.ArtNetInputs = append(keep.ArtNetInputs, u)
|
||||
if merge.ArtNetInputs != nil {
|
||||
if keep.ArtNetInputs == nil {
|
||||
keep.ArtNetInputs = ArtNetUniverseSet{}
|
||||
}
|
||||
for u, lastSeen := range merge.ArtNetInputs {
|
||||
if existing, ok := keep.ArtNetInputs[u]; !ok || lastSeen.After(existing) {
|
||||
keep.ArtNetInputs[u] = lastSeen
|
||||
}
|
||||
}
|
||||
for _, u := range merge.ArtNetOutputs {
|
||||
if !containsInt(keep.ArtNetOutputs, u) {
|
||||
keep.ArtNetOutputs = append(keep.ArtNetOutputs, u)
|
||||
}
|
||||
if merge.ArtNetOutputs != nil {
|
||||
if keep.ArtNetOutputs == nil {
|
||||
keep.ArtNetOutputs = ArtNetUniverseSet{}
|
||||
}
|
||||
for u, lastSeen := range merge.ArtNetOutputs {
|
||||
if existing, ok := keep.ArtNetOutputs[u]; !ok || lastSeen.After(existing) {
|
||||
keep.ArtNetOutputs[u] = lastSeen
|
||||
}
|
||||
}
|
||||
if merge.artnetLastSeen.After(keep.artnetLastSeen) {
|
||||
keep.artnetLastSeen = merge.artnetLastSeen
|
||||
}
|
||||
sort.Ints(keep.ArtNetInputs)
|
||||
sort.Ints(keep.ArtNetOutputs)
|
||||
}
|
||||
|
||||
func (n *Nodes) logArtNet() {
|
||||
inputUniverses := map[int][]string{}
|
||||
outputUniverses := map[int][]string{}
|
||||
inputUniverses := map[ArtNetUniverse][]string{}
|
||||
outputUniverses := map[ArtNetUniverse][]string{}
|
||||
|
||||
for _, node := range n.nodes {
|
||||
if len(node.ArtNetInputs) == 0 && len(node.ArtNetOutputs) == 0 {
|
||||
@@ -238,10 +243,10 @@ func (n *Nodes) logArtNet() {
|
||||
if name == "" {
|
||||
name = "??"
|
||||
}
|
||||
for _, u := range node.ArtNetInputs {
|
||||
for u := range node.ArtNetInputs {
|
||||
inputUniverses[u] = append(inputUniverses[u], name)
|
||||
}
|
||||
for _, u := range node.ArtNetOutputs {
|
||||
for u := range node.ArtNetOutputs {
|
||||
outputUniverses[u] = append(outputUniverses[u], name)
|
||||
}
|
||||
}
|
||||
@@ -250,8 +255,8 @@ func (n *Nodes) logArtNet() {
|
||||
return
|
||||
}
|
||||
|
||||
var allUniverses []int
|
||||
seen := map[int]bool{}
|
||||
seen := map[ArtNetUniverse]bool{}
|
||||
var allUniverses []ArtNetUniverse
|
||||
for u := range inputUniverses {
|
||||
if !seen[u] {
|
||||
allUniverses = append(allUniverses, u)
|
||||
@@ -264,7 +269,7 @@ func (n *Nodes) logArtNet() {
|
||||
seen[u] = true
|
||||
}
|
||||
}
|
||||
sort.Ints(allUniverses)
|
||||
sort.Slice(allUniverses, func(i, j int) bool { return allUniverses[i] < allUniverses[j] })
|
||||
|
||||
log.Printf("[sigusr1] ================ %d artnet universes ================", len(allUniverses))
|
||||
for _, u := range allUniverses {
|
||||
@@ -279,9 +284,6 @@ func (n *Nodes) logArtNet() {
|
||||
sort.Slice(outs, func(i, j int) bool { return sortorder.NaturalLess(outs[i], outs[j]) })
|
||||
parts = append(parts, fmt.Sprintf("out: %s", strings.Join(outs, ", ")))
|
||||
}
|
||||
netVal := (u >> 8) & 0x7f
|
||||
subnet := (u >> 4) & 0x0f
|
||||
universe := u & 0x0f
|
||||
log.Printf("[sigusr1] artnet:%d (%d/%d/%d) %s", u, netVal, subnet, universe, strings.Join(parts, "; "))
|
||||
log.Printf("[sigusr1] artnet:%d (%s) %s", u, u.String(), strings.Join(parts, "; "))
|
||||
}
|
||||
}
|
||||
|
||||
9
dante.go
9
dante.go
@@ -102,12 +102,15 @@ func (n *Nodes) GetDanteTxDeviceInGroup(groupIP net.IP) *Node {
|
||||
n.mu.RLock()
|
||||
defer n.mu.RUnlock()
|
||||
|
||||
groupName := multicastGroupName(groupIP)
|
||||
group := ParseMulticastGroup(groupIP)
|
||||
groupKey := group.String()
|
||||
for _, node := range n.nodes {
|
||||
if node.DanteTxChannels != "" && containsString(node.MulticastGroups, groupName) {
|
||||
if node.DanteTxChannels != "" && node.MulticastGroups != nil {
|
||||
if _, exists := node.MulticastGroups[groupKey]; exists {
|
||||
return node
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -883,7 +886,7 @@ func (t *Tendrils) probeDanteDeviceWithPort(ip net.IP, port int) {
|
||||
log.Printf("[dante] %s: multicast group %s -> tx device %q", ip, groupIP, sourceName)
|
||||
}
|
||||
if sourceNode == nil {
|
||||
sourceNode = t.nodes.GetOrCreateByName(multicastGroupName(groupIP))
|
||||
sourceNode = t.nodes.GetOrCreateByName(ParseMulticastGroup(groupIP).String())
|
||||
}
|
||||
subscriberNode := t.nodes.GetOrCreateByName(info.Name)
|
||||
t.nodes.UpdateDanteFlow(sourceNode, subscriberNode, "", DanteFlowActive)
|
||||
|
||||
30
errors.go
30
errors.go
@@ -15,7 +15,7 @@ const (
|
||||
|
||||
type Error struct {
|
||||
ID string `json:"id"`
|
||||
NodeTypeID string `json:"node_typeid"`
|
||||
NodeID string `json:"node_id"`
|
||||
NodeName string `json:"node_name"`
|
||||
Type string `json:"type"`
|
||||
Port string `json:"port,omitempty"`
|
||||
@@ -88,7 +88,7 @@ func (e *ErrorTracker) checkUtilizationLocked(node *Node, portName string, stats
|
||||
speedBytes := float64(stats.Speed) / 8.0
|
||||
utilization := (maxBytesRate / speedBytes) * 100.0
|
||||
|
||||
key := "util:" + node.TypeID + ":" + portName
|
||||
key := "util:" + node.ID + ":" + portName
|
||||
now := time.Now()
|
||||
|
||||
if utilization < 70.0 {
|
||||
@@ -107,7 +107,7 @@ func (e *ErrorTracker) checkUtilizationLocked(node *Node, portName string, stats
|
||||
e.nextID++
|
||||
e.errors[key] = &Error{
|
||||
ID: fmt.Sprintf("err-%d", e.nextID),
|
||||
NodeTypeID: node.TypeID,
|
||||
NodeID: node.ID,
|
||||
NodeName: node.DisplayName(),
|
||||
Port: portName,
|
||||
Type: ErrorTypeHighUtilization,
|
||||
@@ -122,7 +122,7 @@ func (e *ErrorTracker) checkPortLocked(node *Node, portName string, stats *Inter
|
||||
e.mu.Lock()
|
||||
defer e.mu.Unlock()
|
||||
|
||||
key := node.TypeID + ":" + portName
|
||||
key := node.ID + ":" + portName
|
||||
baseline := e.baselines[key]
|
||||
|
||||
now := time.Now()
|
||||
@@ -137,7 +137,7 @@ func (e *ErrorTracker) checkPortLocked(node *Node, portName string, stats *Inter
|
||||
e.nextID++
|
||||
e.errors[key] = &Error{
|
||||
ID: fmt.Sprintf("err-%d", e.nextID),
|
||||
NodeTypeID: node.TypeID,
|
||||
NodeID: node.ID,
|
||||
NodeName: node.DisplayName(),
|
||||
Port: portName,
|
||||
Type: ErrorTypeStartup,
|
||||
@@ -172,7 +172,7 @@ func (e *ErrorTracker) checkPortLocked(node *Node, portName string, stats *Inter
|
||||
e.nextID++
|
||||
e.errors[key] = &Error{
|
||||
ID: fmt.Sprintf("err-%d", e.nextID),
|
||||
NodeTypeID: node.TypeID,
|
||||
NodeID: node.ID,
|
||||
NodeName: node.DisplayName(),
|
||||
Port: portName,
|
||||
Type: ErrorTypeNew,
|
||||
@@ -253,8 +253,8 @@ func (e *ErrorTracker) GetUnreachableNodeSet() map[string]bool {
|
||||
defer e.mu.RUnlock()
|
||||
|
||||
result := map[string]bool{}
|
||||
for nodeTypeID := range e.unreachableNodes {
|
||||
result[nodeTypeID] = true
|
||||
for nodeID := range e.unreachableNodes {
|
||||
result[nodeID] = true
|
||||
}
|
||||
return result
|
||||
}
|
||||
@@ -271,10 +271,10 @@ func (e *ErrorTracker) setUnreachableLocked(node *Node) (changed bool, becameUnr
|
||||
e.mu.Lock()
|
||||
defer e.mu.Unlock()
|
||||
|
||||
key := "unreachable:" + node.TypeID
|
||||
key := "unreachable:" + node.ID
|
||||
|
||||
wasUnreachable := e.unreachableNodes[node.TypeID]
|
||||
e.unreachableNodes[node.TypeID] = true
|
||||
wasUnreachable := e.unreachableNodes[node.ID]
|
||||
e.unreachableNodes[node.ID] = true
|
||||
becameUnreachable = !wasUnreachable
|
||||
|
||||
if e.suppressedUnreachable[key] {
|
||||
@@ -289,7 +289,7 @@ func (e *ErrorTracker) setUnreachableLocked(node *Node) (changed bool, becameUnr
|
||||
e.nextID++
|
||||
e.errors[key] = &Error{
|
||||
ID: fmt.Sprintf("err-%d", e.nextID),
|
||||
NodeTypeID: node.TypeID,
|
||||
NodeID: node.ID,
|
||||
NodeName: node.DisplayName(),
|
||||
Type: ErrorTypeUnreachable,
|
||||
FirstSeen: now,
|
||||
@@ -310,12 +310,12 @@ func (e *ErrorTracker) clearUnreachableLocked(node *Node) (changed bool, becameR
|
||||
e.mu.Lock()
|
||||
defer e.mu.Unlock()
|
||||
|
||||
key := "unreachable:" + node.TypeID
|
||||
key := "unreachable:" + node.ID
|
||||
|
||||
delete(e.suppressedUnreachable, key)
|
||||
|
||||
wasUnreachable := e.unreachableNodes[node.TypeID]
|
||||
delete(e.unreachableNodes, node.TypeID)
|
||||
wasUnreachable := e.unreachableNodes[node.ID]
|
||||
delete(e.unreachableNodes, node.ID)
|
||||
becameReachable = wasUnreachable
|
||||
|
||||
if _, exists := e.errors[key]; exists {
|
||||
|
||||
2
http.go
2
http.go
@@ -230,7 +230,7 @@ func (t *Tendrils) getNodes() []*Node {
|
||||
for _, node := range t.nodes.nodes {
|
||||
n := new(Node)
|
||||
*n = *node
|
||||
n.Unreachable = unreachableNodes[node.TypeID]
|
||||
n.Unreachable = unreachableNodes[node.ID]
|
||||
nodes = append(nodes, n)
|
||||
}
|
||||
|
||||
|
||||
8
link.go
8
link.go
@@ -6,7 +6,7 @@ import (
|
||||
)
|
||||
|
||||
type Link struct {
|
||||
TypeID string `json:"typeid"`
|
||||
ID string `json:"id"`
|
||||
NodeA *Node `json:"node_a"`
|
||||
InterfaceA string `json:"interface_a,omitempty"`
|
||||
NodeB *Node `json:"node_b"`
|
||||
@@ -15,7 +15,7 @@ type Link struct {
|
||||
|
||||
func (l *Link) MarshalJSON() ([]byte, error) {
|
||||
type linkJSON struct {
|
||||
TypeID string `json:"typeid"`
|
||||
ID string `json:"id"`
|
||||
NodeA interface{} `json:"node_a"`
|
||||
InterfaceA string `json:"interface_a,omitempty"`
|
||||
NodeB interface{} `json:"node_b"`
|
||||
@@ -23,7 +23,7 @@ func (l *Link) MarshalJSON() ([]byte, error) {
|
||||
}
|
||||
|
||||
return json.Marshal(linkJSON{
|
||||
TypeID: l.TypeID,
|
||||
ID: l.ID,
|
||||
NodeA: l.NodeA.WithInterface(l.InterfaceA),
|
||||
InterfaceA: l.InterfaceA,
|
||||
NodeB: l.NodeB.WithInterface(l.InterfaceB),
|
||||
@@ -83,7 +83,7 @@ func (n *Nodes) getDirectLinks() []*Link {
|
||||
if !seen[key] {
|
||||
seen[key] = true
|
||||
links = append(links, &Link{
|
||||
TypeID: newTypeID("link"),
|
||||
ID: newID("link"),
|
||||
NodeA: lh.node,
|
||||
InterfaceA: lh.port,
|
||||
NodeB: target,
|
||||
|
||||
172
multicast.go
172
multicast.go
@@ -1,80 +1,10 @@
|
||||
package tendrils
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net"
|
||||
"sort"
|
||||
"time"
|
||||
)
|
||||
|
||||
type MulticastGroup struct {
|
||||
Name string `json:"name"`
|
||||
IP string `json:"ip"`
|
||||
}
|
||||
|
||||
func (g *MulticastGroup) IsDante() bool {
|
||||
ip := net.ParseIP(g.IP).To4()
|
||||
if ip == nil {
|
||||
return false
|
||||
}
|
||||
if ip[0] == 239 && ip[1] >= 69 && ip[1] <= 71 {
|
||||
return true
|
||||
}
|
||||
if ip[0] == 239 && ip[1] == 253 {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func multicastGroupName(ip net.IP) string {
|
||||
ip4 := ip.To4()
|
||||
if ip4 == nil {
|
||||
return ip.String()
|
||||
}
|
||||
|
||||
switch ip.String() {
|
||||
case "224.0.0.251":
|
||||
return "mdns"
|
||||
case "224.0.1.129":
|
||||
return "ptp"
|
||||
case "224.0.1.130":
|
||||
return "ptp-announce"
|
||||
case "224.0.1.131":
|
||||
return "ptp-sync"
|
||||
case "224.0.1.132":
|
||||
return "ptp-delay"
|
||||
case "224.2.127.254":
|
||||
return "sap"
|
||||
case "239.255.254.253":
|
||||
return "shure-slp"
|
||||
case "239.255.255.250":
|
||||
return "ssdp"
|
||||
case "239.255.255.253":
|
||||
return "slp"
|
||||
case "239.255.255.255":
|
||||
return "admin-scoped-broadcast"
|
||||
}
|
||||
|
||||
if ip4[0] == 239 && ip4[1] == 255 {
|
||||
universe := int(ip4[2])*256 + int(ip4[3])
|
||||
if universe >= 1 && universe <= 63999 {
|
||||
return fmt.Sprintf("sacn:%d", universe)
|
||||
}
|
||||
}
|
||||
|
||||
if ip4[0] == 239 && ip4[1] >= 69 && ip4[1] <= 71 {
|
||||
flowID := (int(ip4[1]-69) << 16) | (int(ip4[2]) << 8) | int(ip4[3])
|
||||
return fmt.Sprintf("dante-mcast:%d", flowID)
|
||||
}
|
||||
|
||||
if ip4[0] == 239 && ip4[1] == 253 {
|
||||
flowID := (int(ip4[2]) << 8) | int(ip4[3])
|
||||
return fmt.Sprintf("dante-av:%d", flowID)
|
||||
}
|
||||
|
||||
return ip.String()
|
||||
}
|
||||
|
||||
func (n *Nodes) UpdateMulticastMembership(sourceIP, groupIP net.IP) {
|
||||
n.mu.Lock()
|
||||
defer n.mu.Unlock()
|
||||
@@ -84,27 +14,12 @@ func (n *Nodes) UpdateMulticastMembership(sourceIP, groupIP net.IP) {
|
||||
return
|
||||
}
|
||||
|
||||
groupName := multicastGroupName(groupIP)
|
||||
group := ParseMulticastGroup(groupIP)
|
||||
|
||||
if node.multicastLastSeen == nil {
|
||||
node.multicastLastSeen = map[string]time.Time{}
|
||||
}
|
||||
node.multicastLastSeen[groupName] = time.Now()
|
||||
|
||||
if !containsString(node.MulticastGroups, groupName) {
|
||||
node.MulticastGroups = append(node.MulticastGroups, groupName)
|
||||
sort.Strings(node.MulticastGroups)
|
||||
}
|
||||
|
||||
if len(groupName) > 5 && groupName[:5] == "sacn:" {
|
||||
var universe int
|
||||
if _, err := fmt.Sscanf(groupName, "sacn:%d", &universe); err == nil {
|
||||
if !containsInt(node.SACNInputs, universe) {
|
||||
node.SACNInputs = append(node.SACNInputs, universe)
|
||||
sort.Ints(node.SACNInputs)
|
||||
}
|
||||
}
|
||||
if node.MulticastGroups == nil {
|
||||
node.MulticastGroups = MulticastMembershipSet{}
|
||||
}
|
||||
node.MulticastGroups.Add(group)
|
||||
}
|
||||
|
||||
func (n *Nodes) RemoveMulticastMembership(sourceIP, groupIP net.IP) {
|
||||
@@ -116,16 +31,12 @@ func (n *Nodes) RemoveMulticastMembership(sourceIP, groupIP net.IP) {
|
||||
return
|
||||
}
|
||||
|
||||
groupName := multicastGroupName(groupIP)
|
||||
delete(node.multicastLastSeen, groupName)
|
||||
if node.MulticastGroups == nil {
|
||||
return
|
||||
}
|
||||
|
||||
var groups []string
|
||||
for _, g := range node.MulticastGroups {
|
||||
if g != groupName {
|
||||
groups = append(groups, g)
|
||||
}
|
||||
}
|
||||
node.MulticastGroups = groups
|
||||
group := ParseMulticastGroup(groupIP)
|
||||
node.MulticastGroups.Remove(group)
|
||||
}
|
||||
|
||||
func (n *Nodes) GetDanteMulticastGroups(deviceIP net.IP) []net.IP {
|
||||
@@ -133,15 +44,14 @@ func (n *Nodes) GetDanteMulticastGroups(deviceIP net.IP) []net.IP {
|
||||
defer n.mu.RUnlock()
|
||||
|
||||
node := n.getNodeByIPLocked(deviceIP)
|
||||
if node == nil {
|
||||
if node == nil || node.MulticastGroups == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
var groups []net.IP
|
||||
for _, groupName := range node.MulticastGroups {
|
||||
g := &MulticastGroup{Name: groupName}
|
||||
if g.IsDante() {
|
||||
ip := net.ParseIP(groupName)
|
||||
for _, group := range node.MulticastGroups.Groups() {
|
||||
if group.IsDante() {
|
||||
ip := net.ParseIP(group.String())
|
||||
if ip != nil {
|
||||
groups = append(groups, ip)
|
||||
}
|
||||
@@ -154,10 +64,14 @@ func (n *Nodes) GetMulticastGroupMembers(groupIP net.IP) []*Node {
|
||||
n.mu.RLock()
|
||||
defer n.mu.RUnlock()
|
||||
|
||||
groupName := multicastGroupName(groupIP)
|
||||
group := ParseMulticastGroup(groupIP)
|
||||
groupKey := group.String()
|
||||
var members []*Node
|
||||
for _, node := range n.nodes {
|
||||
if containsString(node.MulticastGroups, groupName) {
|
||||
if node.MulticastGroups == nil {
|
||||
continue
|
||||
}
|
||||
if _, exists := node.MulticastGroups[groupKey]; exists {
|
||||
members = append(members, node)
|
||||
}
|
||||
}
|
||||
@@ -165,55 +79,23 @@ func (n *Nodes) GetMulticastGroupMembers(groupIP net.IP) []*Node {
|
||||
}
|
||||
|
||||
func (n *Nodes) expireMulticastMemberships() {
|
||||
expireTime := time.Now().Add(-5 * time.Minute)
|
||||
for _, node := range n.nodes {
|
||||
if node.multicastLastSeen == nil {
|
||||
continue
|
||||
if node.MulticastGroups != nil {
|
||||
node.MulticastGroups.Expire(5 * time.Minute)
|
||||
}
|
||||
var keepGroups []string
|
||||
var keepSACNInputs []int
|
||||
for _, groupName := range node.MulticastGroups {
|
||||
if lastSeen, ok := node.multicastLastSeen[groupName]; ok && !lastSeen.Before(expireTime) {
|
||||
keepGroups = append(keepGroups, groupName)
|
||||
if len(groupName) > 5 && groupName[:5] == "sacn:" {
|
||||
var universe int
|
||||
if _, err := fmt.Sscanf(groupName, "sacn:%d", &universe); err == nil {
|
||||
keepSACNInputs = append(keepSACNInputs, universe)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
delete(node.multicastLastSeen, groupName)
|
||||
}
|
||||
}
|
||||
node.MulticastGroups = keepGroups
|
||||
sort.Ints(keepSACNInputs)
|
||||
node.SACNInputs = keepSACNInputs
|
||||
}
|
||||
}
|
||||
|
||||
func (n *Nodes) mergeMulticast(keep, merge *Node) {
|
||||
if merge.multicastLastSeen == nil {
|
||||
if merge.MulticastGroups == nil {
|
||||
return
|
||||
}
|
||||
if keep.multicastLastSeen == nil {
|
||||
keep.multicastLastSeen = map[string]time.Time{}
|
||||
if keep.MulticastGroups == nil {
|
||||
keep.MulticastGroups = MulticastMembershipSet{}
|
||||
}
|
||||
for groupName, lastSeen := range merge.multicastLastSeen {
|
||||
if existing, ok := keep.multicastLastSeen[groupName]; !ok || lastSeen.After(existing) {
|
||||
keep.multicastLastSeen[groupName] = lastSeen
|
||||
}
|
||||
if !containsString(keep.MulticastGroups, groupName) {
|
||||
keep.MulticastGroups = append(keep.MulticastGroups, groupName)
|
||||
}
|
||||
if len(groupName) > 5 && groupName[:5] == "sacn:" {
|
||||
var universe int
|
||||
if _, err := fmt.Sscanf(groupName, "sacn:%d", &universe); err == nil {
|
||||
if !containsInt(keep.SACNInputs, universe) {
|
||||
keep.SACNInputs = append(keep.SACNInputs, universe)
|
||||
for key, membership := range merge.MulticastGroups {
|
||||
if existing, ok := keep.MulticastGroups[key]; !ok || membership.LastSeen.After(existing.LastSeen) {
|
||||
keep.MulticastGroups[key] = membership
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
sort.Strings(keep.MulticastGroups)
|
||||
sort.Ints(keep.SACNInputs)
|
||||
}
|
||||
|
||||
263
nodes.go
263
nodes.go
@@ -15,12 +15,10 @@ import (
|
||||
|
||||
type Nodes struct {
|
||||
mu sync.RWMutex
|
||||
nodes map[int]*Node
|
||||
ipIndex map[string]int
|
||||
macIndex map[string]int
|
||||
nameIndex map[string]int
|
||||
nodeCancel map[int]context.CancelFunc
|
||||
nextID int
|
||||
nodes []*Node
|
||||
ipIndex map[string]*Node
|
||||
macIndex map[string]*Node
|
||||
nameIndex map[string]*Node
|
||||
t *Tendrils
|
||||
ctx context.Context
|
||||
cancelAll context.CancelFunc
|
||||
@@ -29,12 +27,9 @@ type Nodes struct {
|
||||
func NewNodes(t *Tendrils) *Nodes {
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
return &Nodes{
|
||||
nodes: map[int]*Node{},
|
||||
ipIndex: map[string]int{},
|
||||
macIndex: map[string]int{},
|
||||
nameIndex: map[string]int{},
|
||||
nodeCancel: map[int]context.CancelFunc{},
|
||||
nextID: 1,
|
||||
ipIndex: map[string]*Node{},
|
||||
macIndex: map[string]*Node{},
|
||||
nameIndex: map[string]*Node{},
|
||||
t: t,
|
||||
ctx: ctx,
|
||||
cancelAll: cancel,
|
||||
@@ -60,10 +55,9 @@ func (n *Nodes) updateLocked(target *Node, mac net.HardwareAddr, ips []net.IP, i
|
||||
return false
|
||||
}
|
||||
|
||||
targetID, isNew := n.resolveTargetNode(target, mac, ips, nodeName)
|
||||
node := n.nodes[targetID]
|
||||
node, isNew := n.resolveTargetNode(target, mac, ips, nodeName)
|
||||
|
||||
added := n.applyNodeUpdates(node, targetID, mac, ips, ifaceName, nodeName)
|
||||
added := n.applyNodeUpdates(node, mac, ips, ifaceName, nodeName)
|
||||
|
||||
n.logUpdates(node, added, isNew, source)
|
||||
|
||||
@@ -74,108 +68,82 @@ func (n *Nodes) updateLocked(target *Node, mac net.HardwareAddr, ips []net.IP, i
|
||||
return isNew || len(added) > 0
|
||||
}
|
||||
|
||||
func (n *Nodes) resolveTargetNode(target *Node, mac net.HardwareAddr, ips []net.IP, nodeName string) (int, bool) {
|
||||
targetID := n.findByTarget(target)
|
||||
targetID = n.findOrMergeByMAC(targetID, mac)
|
||||
if targetID == -1 {
|
||||
targetID = n.findByIPs(ips)
|
||||
func (n *Nodes) resolveTargetNode(target *Node, mac net.HardwareAddr, ips []net.IP, nodeName string) (*Node, bool) {
|
||||
node := n.findOrMergeByMAC(target, mac)
|
||||
if node == nil {
|
||||
node = n.findByIPs(ips)
|
||||
}
|
||||
targetID = n.findOrMergeByName(targetID, nodeName)
|
||||
node = n.findOrMergeByName(node, nodeName)
|
||||
|
||||
if targetID == -1 {
|
||||
if node == nil {
|
||||
return n.createNode(), true
|
||||
}
|
||||
return targetID, false
|
||||
return node, false
|
||||
}
|
||||
|
||||
func (n *Nodes) findByTarget(target *Node) int {
|
||||
if target == nil {
|
||||
return -1
|
||||
}
|
||||
for id, node := range n.nodes {
|
||||
if node == target {
|
||||
return id
|
||||
}
|
||||
}
|
||||
return -1
|
||||
}
|
||||
|
||||
func (n *Nodes) findOrMergeByMAC(targetID int, mac net.HardwareAddr) int {
|
||||
func (n *Nodes) findOrMergeByMAC(target *Node, mac net.HardwareAddr) *Node {
|
||||
if mac == nil {
|
||||
return targetID
|
||||
return target
|
||||
}
|
||||
macKey := mac.String()
|
||||
id, exists := n.macIndex[macKey]
|
||||
if !exists {
|
||||
return targetID
|
||||
found := n.macIndex[macKey]
|
||||
if found == nil {
|
||||
return target
|
||||
}
|
||||
if _, nodeExists := n.nodes[id]; !nodeExists {
|
||||
delete(n.macIndex, macKey)
|
||||
return targetID
|
||||
if target == nil {
|
||||
return found
|
||||
}
|
||||
if targetID == -1 {
|
||||
return id
|
||||
if found != target {
|
||||
n.mergeNodes(target, found)
|
||||
}
|
||||
if id != targetID {
|
||||
n.mergeNodes(targetID, id)
|
||||
}
|
||||
return targetID
|
||||
return target
|
||||
}
|
||||
|
||||
func (n *Nodes) findByIPs(ips []net.IP) int {
|
||||
func (n *Nodes) findByIPs(ips []net.IP) *Node {
|
||||
for _, ip := range ips {
|
||||
if id, exists := n.ipIndex[ip.String()]; exists {
|
||||
if _, nodeExists := n.nodes[id]; nodeExists {
|
||||
return id
|
||||
if node := n.ipIndex[ip.String()]; node != nil {
|
||||
return node
|
||||
}
|
||||
}
|
||||
}
|
||||
return -1
|
||||
return nil
|
||||
}
|
||||
|
||||
func (n *Nodes) findOrMergeByName(targetID int, nodeName string) int {
|
||||
func (n *Nodes) findOrMergeByName(target *Node, nodeName string) *Node {
|
||||
if nodeName == "" {
|
||||
return targetID
|
||||
return target
|
||||
}
|
||||
id, exists := n.nameIndex[nodeName]
|
||||
if !exists {
|
||||
return targetID
|
||||
found := n.nameIndex[nodeName]
|
||||
if found == nil {
|
||||
return target
|
||||
}
|
||||
nameNode, nodeExists := n.nodes[id]
|
||||
if !nodeExists {
|
||||
delete(n.nameIndex, nodeName)
|
||||
return targetID
|
||||
if target == nil {
|
||||
return found
|
||||
}
|
||||
if targetID == -1 {
|
||||
return id
|
||||
if found != target && len(found.Interfaces) == 0 {
|
||||
n.mergeNodes(target, found)
|
||||
}
|
||||
if id != targetID && len(nameNode.Interfaces) == 0 {
|
||||
n.mergeNodes(targetID, id)
|
||||
}
|
||||
return targetID
|
||||
return target
|
||||
}
|
||||
|
||||
func (n *Nodes) createNode() int {
|
||||
targetID := n.nextID
|
||||
n.nextID++
|
||||
func (n *Nodes) createNode() *Node {
|
||||
node := &Node{
|
||||
TypeID: newTypeID("node"),
|
||||
ID: newID("node"),
|
||||
Interfaces: InterfaceMap{},
|
||||
MACTable: map[string]string{},
|
||||
pollTrigger: make(chan struct{}, 1),
|
||||
}
|
||||
n.nodes[targetID] = node
|
||||
n.startNodePoller(targetID, node)
|
||||
return targetID
|
||||
n.nodes = append(n.nodes, node)
|
||||
n.startNodePoller(node)
|
||||
return node
|
||||
}
|
||||
|
||||
func (n *Nodes) applyNodeUpdates(node *Node, nodeID int, mac net.HardwareAddr, ips []net.IP, ifaceName, nodeName string) []string {
|
||||
func (n *Nodes) applyNodeUpdates(node *Node, mac net.HardwareAddr, ips []net.IP, ifaceName, nodeName string) []string {
|
||||
var added []string
|
||||
|
||||
if mac != nil {
|
||||
added = n.updateNodeInterface(node, nodeID, mac, ips, ifaceName)
|
||||
added = n.updateNodeInterface(node, mac, ips, ifaceName)
|
||||
} else {
|
||||
added = n.updateNodeIPs(node, nodeID, ips)
|
||||
added = n.updateNodeIPs(node, ips)
|
||||
}
|
||||
|
||||
if nodeName != "" {
|
||||
@@ -184,7 +152,7 @@ func (n *Nodes) applyNodeUpdates(node *Node, nodeID int, mac net.HardwareAddr, i
|
||||
}
|
||||
if !node.Names.Has(nodeName) {
|
||||
node.Names.Add(nodeName)
|
||||
n.nameIndex[nodeName] = nodeID
|
||||
n.nameIndex[nodeName] = node
|
||||
added = append(added, "name="+nodeName)
|
||||
}
|
||||
}
|
||||
@@ -192,22 +160,17 @@ func (n *Nodes) applyNodeUpdates(node *Node, nodeID int, mac net.HardwareAddr, i
|
||||
return added
|
||||
}
|
||||
|
||||
func (n *Nodes) updateNodeIPs(node *Node, nodeID int, ips []net.IP) []string {
|
||||
func (n *Nodes) updateNodeIPs(node *Node, ips []net.IP) []string {
|
||||
var added []string
|
||||
for _, ip := range ips {
|
||||
ipKey := ip.String()
|
||||
if existingID, exists := n.ipIndex[ipKey]; exists {
|
||||
if existingID == nodeID {
|
||||
continue
|
||||
}
|
||||
if existingNode, nodeExists := n.nodes[existingID]; nodeExists {
|
||||
n.mergeNodes(nodeID, existingID)
|
||||
if existing := n.ipIndex[ipKey]; existing != nil && existing != node {
|
||||
n.mergeNodes(node, existing)
|
||||
if n.t.LogEvents {
|
||||
log.Printf("[merge] %s into %s (shared ip %s)", existingNode, node, ipKey)
|
||||
log.Printf("[merge] %s into %s (shared ip %s)", existing, node, ipKey)
|
||||
}
|
||||
}
|
||||
}
|
||||
n.ipIndex[ipKey] = nodeID
|
||||
n.ipIndex[ipKey] = node
|
||||
iface, exists := node.Interfaces[ipKey]
|
||||
if !exists {
|
||||
iface = &Interface{IPs: IPSet{}}
|
||||
@@ -245,9 +208,9 @@ func (n *Nodes) logUpdates(node *Node, added []string, isNew bool, source string
|
||||
}
|
||||
}
|
||||
|
||||
func (n *Nodes) startNodePoller(nodeID int, node *Node) {
|
||||
func (n *Nodes) startNodePoller(node *Node) {
|
||||
ctx, cancel := context.WithCancel(n.ctx)
|
||||
n.nodeCancel[nodeID] = cancel
|
||||
node.cancelFunc = cancel
|
||||
|
||||
go func() {
|
||||
pollTicker := time.NewTicker(10 * time.Second)
|
||||
@@ -277,7 +240,7 @@ func (n *Nodes) triggerPoll(node *Node) {
|
||||
}
|
||||
}
|
||||
|
||||
func (n *Nodes) updateNodeInterface(node *Node, nodeID int, mac net.HardwareAddr, ips []net.IP, ifaceName string) []string {
|
||||
func (n *Nodes) updateNodeInterface(node *Node, mac net.HardwareAddr, ips []net.IP, ifaceName string) []string {
|
||||
macKey := mac.String()
|
||||
var added []string
|
||||
|
||||
@@ -300,25 +263,23 @@ func (n *Nodes) updateNodeInterface(node *Node, nodeID int, mac net.HardwareAddr
|
||||
added = append(added, "iface="+ifaceKey)
|
||||
}
|
||||
|
||||
if _, exists := n.macIndex[macKey]; !exists {
|
||||
n.macIndex[macKey] = nodeID
|
||||
if n.macIndex[macKey] == nil {
|
||||
n.macIndex[macKey] = node
|
||||
}
|
||||
|
||||
for _, ip := range ips {
|
||||
ipKey := ip.String()
|
||||
if existingID, exists := n.ipIndex[ipKey]; exists && existingID != nodeID {
|
||||
if existingNode, nodeExists := n.nodes[existingID]; nodeExists {
|
||||
n.mergeNodes(nodeID, existingID)
|
||||
if existing := n.ipIndex[ipKey]; existing != nil && existing != node {
|
||||
n.mergeNodes(node, existing)
|
||||
if n.t.LogEvents {
|
||||
log.Printf("[merge] %s into %s (shared ip %s)", existingNode, node, ipKey)
|
||||
}
|
||||
log.Printf("[merge] %s into %s (shared ip %s)", existing, node, ipKey)
|
||||
}
|
||||
}
|
||||
if !iface.IPs.Has(ipKey) {
|
||||
added = append(added, "ip="+ipKey)
|
||||
}
|
||||
iface.IPs.Add(ip)
|
||||
n.ipIndex[ipKey] = nodeID
|
||||
n.ipIndex[ipKey] = node
|
||||
|
||||
if ipOnlyIface, exists := node.Interfaces[ipKey]; exists && ipOnlyIface != iface {
|
||||
delete(node.Interfaces, ipKey)
|
||||
@@ -356,40 +317,36 @@ func (n *Nodes) Merge(macs []net.HardwareAddr, source string) {
|
||||
return
|
||||
}
|
||||
|
||||
existingIDs := map[int]bool{}
|
||||
existing := map[*Node]bool{}
|
||||
for _, mac := range macs {
|
||||
if id, exists := n.macIndex[mac.String()]; exists {
|
||||
existingIDs[id] = true
|
||||
if node := n.macIndex[mac.String()]; node != nil {
|
||||
existing[node] = true
|
||||
}
|
||||
}
|
||||
|
||||
if len(existingIDs) < 2 {
|
||||
if len(existing) < 2 {
|
||||
return
|
||||
}
|
||||
|
||||
var ids []int
|
||||
for id := range existingIDs {
|
||||
ids = append(ids, id)
|
||||
var nodes []*Node
|
||||
for node := range existing {
|
||||
nodes = append(nodes, node)
|
||||
}
|
||||
sort.Ints(ids)
|
||||
|
||||
targetID := ids[0]
|
||||
for i := 1; i < len(ids); i++ {
|
||||
target := nodes[0]
|
||||
for i := 1; i < len(nodes); i++ {
|
||||
if n.t.LogEvents {
|
||||
log.Printf("[merge] %s into %s (via %s)", n.nodes[ids[i]], n.nodes[targetID], source)
|
||||
log.Printf("[merge] %s into %s (via %s)", nodes[i], target, source)
|
||||
}
|
||||
n.mergeNodes(targetID, ids[i])
|
||||
n.mergeNodes(target, nodes[i])
|
||||
}
|
||||
|
||||
if n.t.LogNodes {
|
||||
n.logNode(n.nodes[targetID])
|
||||
n.logNode(target)
|
||||
}
|
||||
}
|
||||
|
||||
func (n *Nodes) mergeNodes(keepID, mergeID int) {
|
||||
keep := n.nodes[keepID]
|
||||
merge := n.nodes[mergeID]
|
||||
|
||||
func (n *Nodes) mergeNodes(keep, merge *Node) {
|
||||
if keep == nil || merge == nil {
|
||||
return
|
||||
}
|
||||
@@ -399,7 +356,7 @@ func (n *Nodes) mergeNodes(keepID, mergeID int) {
|
||||
keep.Names = NameSet{}
|
||||
}
|
||||
keep.Names.Add(name)
|
||||
n.nameIndex[name] = keepID
|
||||
n.nameIndex[name] = keep
|
||||
}
|
||||
|
||||
for _, iface := range merge.Interfaces {
|
||||
@@ -408,8 +365,8 @@ func (n *Nodes) mergeNodes(keepID, mergeID int) {
|
||||
ips = append(ips, net.ParseIP(ipStr))
|
||||
}
|
||||
if iface.MAC != "" {
|
||||
n.updateNodeInterface(keep, keepID, iface.MAC.Parse(), ips, iface.Name)
|
||||
n.macIndex[string(iface.MAC)] = keepID
|
||||
n.updateNodeInterface(keep, iface.MAC.Parse(), ips, iface.Name)
|
||||
n.macIndex[string(iface.MAC)] = keep
|
||||
}
|
||||
}
|
||||
|
||||
@@ -425,12 +382,20 @@ func (n *Nodes) mergeNodes(keepID, mergeID int) {
|
||||
n.mergeMulticast(keep, merge)
|
||||
n.mergeDante(keep, merge)
|
||||
|
||||
if cancel, exists := n.nodeCancel[mergeID]; exists {
|
||||
cancel()
|
||||
delete(n.nodeCancel, mergeID)
|
||||
if merge.cancelFunc != nil {
|
||||
merge.cancelFunc()
|
||||
}
|
||||
|
||||
delete(n.nodes, mergeID)
|
||||
n.removeNode(merge)
|
||||
}
|
||||
|
||||
func (n *Nodes) removeNode(node *Node) {
|
||||
for i, nd := range n.nodes {
|
||||
if nd == node {
|
||||
n.nodes = append(n.nodes[:i], n.nodes[i+1:]...)
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (n *Nodes) GetByIP(ip net.IP) *Node {
|
||||
@@ -441,55 +406,41 @@ func (n *Nodes) GetByIP(ip net.IP) *Node {
|
||||
}
|
||||
|
||||
func (n *Nodes) getByIPLocked(ip net.IP) *Node {
|
||||
if id, exists := n.ipIndex[ip.String()]; exists {
|
||||
return n.nodes[id]
|
||||
}
|
||||
return nil
|
||||
return n.ipIndex[ip.String()]
|
||||
}
|
||||
|
||||
func (n *Nodes) GetByMAC(mac net.HardwareAddr) *Node {
|
||||
n.mu.RLock()
|
||||
defer n.mu.RUnlock()
|
||||
|
||||
if id, exists := n.macIndex[mac.String()]; exists {
|
||||
return n.nodes[id]
|
||||
}
|
||||
return nil
|
||||
return n.macIndex[mac.String()]
|
||||
}
|
||||
|
||||
func (n *Nodes) GetByName(name string) *Node {
|
||||
n.mu.RLock()
|
||||
defer n.mu.RUnlock()
|
||||
|
||||
if id, exists := n.nameIndex[name]; exists {
|
||||
return n.nodes[id]
|
||||
}
|
||||
return nil
|
||||
return n.nameIndex[name]
|
||||
}
|
||||
|
||||
func (n *Nodes) GetOrCreateByName(name string) *Node {
|
||||
n.mu.Lock()
|
||||
defer n.mu.Unlock()
|
||||
|
||||
if id, exists := n.nameIndex[name]; exists {
|
||||
if node, nodeExists := n.nodes[id]; nodeExists {
|
||||
if node := n.nameIndex[name]; node != nil {
|
||||
return node
|
||||
}
|
||||
delete(n.nameIndex, name)
|
||||
}
|
||||
|
||||
targetID := n.nextID
|
||||
n.nextID++
|
||||
node := &Node{
|
||||
TypeID: newTypeID("node"),
|
||||
ID: newID("node"),
|
||||
Names: NameSet{name: true},
|
||||
Interfaces: InterfaceMap{},
|
||||
MACTable: map[string]string{},
|
||||
pollTrigger: make(chan struct{}, 1),
|
||||
}
|
||||
n.nodes[targetID] = node
|
||||
n.nameIndex[name] = targetID
|
||||
n.startNodePoller(targetID, node)
|
||||
n.nodes = append(n.nodes, node)
|
||||
n.nameIndex[name] = node
|
||||
n.startNodePoller(node)
|
||||
|
||||
if n.t.LogEvents {
|
||||
log.Printf("[add] %s [name=%s] (via name-lookup)", node, name)
|
||||
@@ -518,16 +469,13 @@ func (n *Nodes) SetDanteClockMaster(ip net.IP) {
|
||||
node.IsDanteClockMaster = false
|
||||
}
|
||||
|
||||
if id, exists := n.ipIndex[ip.String()]; exists {
|
||||
n.nodes[id].IsDanteClockMaster = true
|
||||
if node := n.ipIndex[ip.String()]; node != nil {
|
||||
node.IsDanteClockMaster = true
|
||||
}
|
||||
}
|
||||
|
||||
func (n *Nodes) getNodeByIPLocked(ip net.IP) *Node {
|
||||
if id, exists := n.ipIndex[ip.String()]; exists {
|
||||
return n.nodes[id]
|
||||
}
|
||||
return nil
|
||||
return n.ipIndex[ip.String()]
|
||||
}
|
||||
|
||||
func (n *Nodes) logNode(node *Node) {
|
||||
@@ -620,12 +568,15 @@ func (n *Nodes) LogAll() {
|
||||
|
||||
groupMembers := map[string][]string{}
|
||||
for _, node := range n.nodes {
|
||||
for _, groupName := range node.MulticastGroups {
|
||||
if node.MulticastGroups == nil {
|
||||
continue
|
||||
}
|
||||
for _, group := range node.MulticastGroups.Groups() {
|
||||
name := node.DisplayName()
|
||||
if name == "" {
|
||||
name = "??"
|
||||
}
|
||||
groupMembers[groupName] = append(groupMembers[groupName], name)
|
||||
groupMembers[group.String()] = append(groupMembers[group.String()], name)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
2
ping.go
2
ping.go
@@ -142,7 +142,7 @@ func (t *Tendrils) pingNode(node *Node) {
|
||||
t.nodes.mu.RLock()
|
||||
var ips []string
|
||||
nodeName := node.DisplayName()
|
||||
nodeID := node.TypeID
|
||||
nodeID := node.ID
|
||||
for _, iface := range node.Interfaces {
|
||||
for ipStr := range iface.IPs {
|
||||
ip := net.ParseIP(ipStr)
|
||||
|
||||
@@ -4,7 +4,6 @@ import (
|
||||
"context"
|
||||
"log"
|
||||
"net"
|
||||
"sort"
|
||||
"time"
|
||||
|
||||
"github.com/gopatchy/sacn"
|
||||
@@ -61,29 +60,33 @@ func (n *Nodes) UpdateSACN(node *Node, outputs []int) {
|
||||
n.mu.Lock()
|
||||
defer n.mu.Unlock()
|
||||
|
||||
node.SACNOutputs = outputs
|
||||
sort.Ints(node.SACNOutputs)
|
||||
node.sacnLastSeen = time.Now()
|
||||
if node.SACNOutputs == nil {
|
||||
node.SACNOutputs = SACNUniverseSet{}
|
||||
}
|
||||
|
||||
for _, u := range outputs {
|
||||
node.SACNOutputs.Add(SACNUniverse(u))
|
||||
}
|
||||
}
|
||||
|
||||
func (n *Nodes) expireSACN() {
|
||||
expireTime := time.Now().Add(-60 * time.Second)
|
||||
for _, node := range n.nodes {
|
||||
if !node.sacnLastSeen.IsZero() && node.sacnLastSeen.Before(expireTime) {
|
||||
node.SACNOutputs = nil
|
||||
node.sacnLastSeen = time.Time{}
|
||||
if node.SACNOutputs != nil {
|
||||
node.SACNOutputs.Expire(60 * time.Second)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (n *Nodes) mergeSACN(keep, merge *Node) {
|
||||
for _, u := range merge.SACNOutputs {
|
||||
if !containsInt(keep.SACNOutputs, u) {
|
||||
keep.SACNOutputs = append(keep.SACNOutputs, u)
|
||||
if merge.SACNOutputs == nil {
|
||||
return
|
||||
}
|
||||
if keep.SACNOutputs == nil {
|
||||
keep.SACNOutputs = SACNUniverseSet{}
|
||||
}
|
||||
for u, lastSeen := range merge.SACNOutputs {
|
||||
if existing, ok := keep.SACNOutputs[u]; !ok || lastSeen.After(existing) {
|
||||
keep.SACNOutputs[u] = lastSeen
|
||||
}
|
||||
}
|
||||
if merge.sacnLastSeen.After(keep.sacnLastSeen) {
|
||||
keep.sacnLastSeen = merge.sacnLastSeen
|
||||
}
|
||||
sort.Ints(keep.SACNOutputs)
|
||||
}
|
||||
|
||||
2
snmp.go
2
snmp.go
@@ -246,7 +246,7 @@ func (t *Tendrils) queryInterfaceStats(snmp *gosnmp.GoSNMP, node *Node, ifNames
|
||||
outBytes, hasOutBytes := ifHCOutOctets[ifIndex]
|
||||
|
||||
if hasInPkts && hasOutPkts && hasInBytes && hasOutBytes {
|
||||
key := node.TypeID + ":" + name
|
||||
key := node.ID + ":" + name
|
||||
ifaceTracker.mu.Lock()
|
||||
prev, hasPrev := ifaceTracker.counters[key]
|
||||
if hasPrev {
|
||||
|
||||
329
types.go
329
types.go
@@ -1,6 +1,7 @@
|
||||
package tendrils
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net"
|
||||
@@ -12,11 +13,266 @@ import (
|
||||
"go.jetify.com/typeid"
|
||||
)
|
||||
|
||||
func newTypeID(prefix string) string {
|
||||
func newID(prefix string) string {
|
||||
tid, _ := typeid.WithPrefix(prefix)
|
||||
return tid.String()
|
||||
}
|
||||
|
||||
type ArtNetUniverse int
|
||||
|
||||
func (u ArtNetUniverse) Net() int {
|
||||
return (int(u) >> 8) & 0x7f
|
||||
}
|
||||
|
||||
func (u ArtNetUniverse) Subnet() int {
|
||||
return (int(u) >> 4) & 0x0f
|
||||
}
|
||||
|
||||
func (u ArtNetUniverse) Universe() int {
|
||||
return int(u) & 0x0f
|
||||
}
|
||||
|
||||
func (u ArtNetUniverse) String() string {
|
||||
return fmt.Sprintf("%d/%d/%d", u.Net(), u.Subnet(), u.Universe())
|
||||
}
|
||||
|
||||
type ArtNetUniverseSet map[ArtNetUniverse]time.Time
|
||||
|
||||
func (s ArtNetUniverseSet) Add(u ArtNetUniverse) {
|
||||
s[u] = time.Now()
|
||||
}
|
||||
|
||||
func (s ArtNetUniverseSet) Universes() []ArtNetUniverse {
|
||||
result := make([]ArtNetUniverse, 0, len(s))
|
||||
for u := range s {
|
||||
result = append(result, u)
|
||||
}
|
||||
sort.Slice(result, func(i, j int) bool { return result[i] < result[j] })
|
||||
return result
|
||||
}
|
||||
|
||||
func (s ArtNetUniverseSet) Expire(maxAge time.Duration) {
|
||||
expireTime := time.Now().Add(-maxAge)
|
||||
for u, lastSeen := range s {
|
||||
if lastSeen.Before(expireTime) {
|
||||
delete(s, u)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (s ArtNetUniverseSet) MarshalJSON() ([]byte, error) {
|
||||
return json.Marshal(s.Universes())
|
||||
}
|
||||
|
||||
type SACNUniverse int
|
||||
|
||||
func (u SACNUniverse) String() string {
|
||||
return fmt.Sprintf("%d", u)
|
||||
}
|
||||
|
||||
type SACNUniverseSet map[SACNUniverse]time.Time
|
||||
|
||||
func (s SACNUniverseSet) Add(u SACNUniverse) {
|
||||
s[u] = time.Now()
|
||||
}
|
||||
|
||||
func (s SACNUniverseSet) Universes() []SACNUniverse {
|
||||
result := make([]SACNUniverse, 0, len(s))
|
||||
for u := range s {
|
||||
result = append(result, u)
|
||||
}
|
||||
sort.Slice(result, func(i, j int) bool { return result[i] < result[j] })
|
||||
return result
|
||||
}
|
||||
|
||||
func (s SACNUniverseSet) Expire(maxAge time.Duration) {
|
||||
expireTime := time.Now().Add(-maxAge)
|
||||
for u, lastSeen := range s {
|
||||
if lastSeen.Before(expireTime) {
|
||||
delete(s, u)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (s SACNUniverseSet) MarshalJSON() ([]byte, error) {
|
||||
return json.Marshal(s.Universes())
|
||||
}
|
||||
|
||||
type MulticastGroupID int
|
||||
|
||||
const (
|
||||
MulticastUnknown MulticastGroupID = iota
|
||||
MulticastMDNS
|
||||
MulticastPTP
|
||||
MulticastPTPAnnounce
|
||||
MulticastPTPSync
|
||||
MulticastPTPDelay
|
||||
MulticastSAP
|
||||
MulticastShureSLP
|
||||
MulticastSSDP
|
||||
MulticastSLP
|
||||
MulticastAdminScopedBroadcast
|
||||
)
|
||||
|
||||
type MulticastGroup struct {
|
||||
ID MulticastGroupID
|
||||
SACNUniverse SACNUniverse
|
||||
DanteFlow int
|
||||
DanteAV int
|
||||
RawIP string
|
||||
}
|
||||
|
||||
func (g MulticastGroup) String() string {
|
||||
switch g.ID {
|
||||
case MulticastMDNS:
|
||||
return "mdns"
|
||||
case MulticastPTP:
|
||||
return "ptp"
|
||||
case MulticastPTPAnnounce:
|
||||
return "ptp-announce"
|
||||
case MulticastPTPSync:
|
||||
return "ptp-sync"
|
||||
case MulticastPTPDelay:
|
||||
return "ptp-delay"
|
||||
case MulticastSAP:
|
||||
return "sap"
|
||||
case MulticastShureSLP:
|
||||
return "shure-slp"
|
||||
case MulticastSSDP:
|
||||
return "ssdp"
|
||||
case MulticastSLP:
|
||||
return "slp"
|
||||
case MulticastAdminScopedBroadcast:
|
||||
return "admin-scoped-broadcast"
|
||||
}
|
||||
if g.SACNUniverse > 0 {
|
||||
return fmt.Sprintf("sacn:%d", g.SACNUniverse)
|
||||
}
|
||||
if g.DanteFlow > 0 {
|
||||
return fmt.Sprintf("dante-mcast:%d", g.DanteFlow)
|
||||
}
|
||||
if g.DanteAV > 0 {
|
||||
return fmt.Sprintf("dante-av:%d", g.DanteAV)
|
||||
}
|
||||
return g.RawIP
|
||||
}
|
||||
|
||||
func (g MulticastGroup) MarshalJSON() ([]byte, error) {
|
||||
return json.Marshal(g.String())
|
||||
}
|
||||
|
||||
func (g MulticastGroup) IsDante() bool {
|
||||
return g.DanteFlow > 0 || g.DanteAV > 0
|
||||
}
|
||||
|
||||
func (g MulticastGroup) IsSACN() bool {
|
||||
return g.SACNUniverse > 0
|
||||
}
|
||||
|
||||
func ParseMulticastGroup(ip net.IP) MulticastGroup {
|
||||
ip4 := ip.To4()
|
||||
if ip4 == nil {
|
||||
return MulticastGroup{RawIP: ip.String()}
|
||||
}
|
||||
|
||||
switch ip.String() {
|
||||
case "224.0.0.251":
|
||||
return MulticastGroup{ID: MulticastMDNS}
|
||||
case "224.0.1.129":
|
||||
return MulticastGroup{ID: MulticastPTP}
|
||||
case "224.0.1.130":
|
||||
return MulticastGroup{ID: MulticastPTPAnnounce}
|
||||
case "224.0.1.131":
|
||||
return MulticastGroup{ID: MulticastPTPSync}
|
||||
case "224.0.1.132":
|
||||
return MulticastGroup{ID: MulticastPTPDelay}
|
||||
case "224.2.127.254":
|
||||
return MulticastGroup{ID: MulticastSAP}
|
||||
case "239.255.254.253":
|
||||
return MulticastGroup{ID: MulticastShureSLP}
|
||||
case "239.255.255.250":
|
||||
return MulticastGroup{ID: MulticastSSDP}
|
||||
case "239.255.255.253":
|
||||
return MulticastGroup{ID: MulticastSLP}
|
||||
case "239.255.255.255":
|
||||
return MulticastGroup{ID: MulticastAdminScopedBroadcast}
|
||||
}
|
||||
|
||||
if ip4[0] == 239 && ip4[1] == 255 {
|
||||
universe := int(ip4[2])*256 + int(ip4[3])
|
||||
if universe >= 1 && universe <= 63999 {
|
||||
return MulticastGroup{SACNUniverse: SACNUniverse(universe)}
|
||||
}
|
||||
}
|
||||
|
||||
if ip4[0] == 239 && ip4[1] >= 69 && ip4[1] <= 71 {
|
||||
flowID := (int(ip4[1]-69) << 16) | (int(ip4[2]) << 8) | int(ip4[3])
|
||||
return MulticastGroup{DanteFlow: flowID}
|
||||
}
|
||||
|
||||
if ip4[0] == 239 && ip4[1] == 253 {
|
||||
flowID := (int(ip4[2]) << 8) | int(ip4[3])
|
||||
return MulticastGroup{DanteAV: flowID}
|
||||
}
|
||||
|
||||
return MulticastGroup{RawIP: ip.String()}
|
||||
}
|
||||
|
||||
type MulticastMembership struct {
|
||||
Group MulticastGroup
|
||||
LastSeen time.Time
|
||||
}
|
||||
|
||||
type MulticastMembershipSet map[string]*MulticastMembership
|
||||
|
||||
func (s MulticastMembershipSet) Add(group MulticastGroup) {
|
||||
key := group.String()
|
||||
if m, exists := s[key]; exists {
|
||||
m.LastSeen = time.Now()
|
||||
} else {
|
||||
s[key] = &MulticastMembership{Group: group, LastSeen: time.Now()}
|
||||
}
|
||||
}
|
||||
|
||||
func (s MulticastMembershipSet) Remove(group MulticastGroup) {
|
||||
delete(s, group.String())
|
||||
}
|
||||
|
||||
func (s MulticastMembershipSet) Groups() []MulticastGroup {
|
||||
result := make([]MulticastGroup, 0, len(s))
|
||||
for _, m := range s {
|
||||
result = append(result, m.Group)
|
||||
}
|
||||
sort.Slice(result, func(i, j int) bool {
|
||||
return result[i].String() < result[j].String()
|
||||
})
|
||||
return result
|
||||
}
|
||||
|
||||
func (s MulticastMembershipSet) SACNInputs() []SACNUniverse {
|
||||
var result []SACNUniverse
|
||||
for _, m := range s {
|
||||
if m.Group.IsSACN() {
|
||||
result = append(result, m.Group.SACNUniverse)
|
||||
}
|
||||
}
|
||||
sort.Slice(result, func(i, j int) bool { return result[i] < result[j] })
|
||||
return result
|
||||
}
|
||||
|
||||
func (s MulticastMembershipSet) Expire(maxAge time.Duration) {
|
||||
expireTime := time.Now().Add(-maxAge)
|
||||
for key, m := range s {
|
||||
if m.LastSeen.Before(expireTime) {
|
||||
delete(s, key)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (s MulticastMembershipSet) MarshalJSON() ([]byte, error) {
|
||||
return json.Marshal(s.Groups())
|
||||
}
|
||||
|
||||
type MAC string
|
||||
|
||||
func (m MAC) Parse() net.HardwareAddr {
|
||||
@@ -133,30 +389,73 @@ type PoEBudget struct {
|
||||
}
|
||||
|
||||
type Node struct {
|
||||
TypeID string `json:"typeid"`
|
||||
ID string `json:"id"`
|
||||
Names NameSet `json:"names"`
|
||||
Interfaces InterfaceMap `json:"interfaces"`
|
||||
MACTable map[string]string `json:"-"`
|
||||
MACTableSize int `json:"mac_table_size,omitempty"`
|
||||
PoEBudget *PoEBudget `json:"poe_budget,omitempty"`
|
||||
IsDanteClockMaster bool `json:"is_dante_clock_master,omitempty"`
|
||||
DanteTxChannels string `json:"dante_tx_channels,omitempty"`
|
||||
MulticastGroups []string `json:"multicast_groups,omitempty"`
|
||||
ArtNetInputs []int `json:"artnet_inputs,omitempty"`
|
||||
ArtNetOutputs []int `json:"artnet_outputs,omitempty"`
|
||||
SACNInputs []int `json:"sacn_inputs,omitempty"`
|
||||
SACNOutputs []int `json:"sacn_outputs,omitempty"`
|
||||
MulticastGroups MulticastMembershipSet `json:"multicast_groups,omitempty"`
|
||||
ArtNetInputs ArtNetUniverseSet `json:"artnet_inputs,omitempty"`
|
||||
ArtNetOutputs ArtNetUniverseSet `json:"artnet_outputs,omitempty"`
|
||||
SACNOutputs SACNUniverseSet `json:"sacn_outputs,omitempty"`
|
||||
DanteTx []*DantePeer `json:"dante_tx,omitempty"`
|
||||
DanteRx []*DantePeer `json:"dante_rx,omitempty"`
|
||||
Unreachable bool `json:"unreachable,omitempty"`
|
||||
pollTrigger chan struct{}
|
||||
|
||||
multicastLastSeen map[string]time.Time
|
||||
artnetLastSeen time.Time
|
||||
sacnLastSeen time.Time
|
||||
cancelFunc context.CancelFunc
|
||||
danteLastSeen time.Time
|
||||
}
|
||||
|
||||
func (n *Node) MACTableSize() int {
|
||||
return len(n.MACTable)
|
||||
}
|
||||
|
||||
func (n *Node) SACNInputs() []SACNUniverse {
|
||||
if n.MulticastGroups == nil {
|
||||
return nil
|
||||
}
|
||||
return n.MulticastGroups.SACNInputs()
|
||||
}
|
||||
|
||||
func (n *Node) MarshalJSON() ([]byte, error) {
|
||||
type nodeJSON struct {
|
||||
ID string `json:"id"`
|
||||
Names NameSet `json:"names"`
|
||||
Interfaces InterfaceMap `json:"interfaces"`
|
||||
MACTableSize int `json:"mac_table_size,omitempty"`
|
||||
PoEBudget *PoEBudget `json:"poe_budget,omitempty"`
|
||||
IsDanteClockMaster bool `json:"is_dante_clock_master,omitempty"`
|
||||
DanteTxChannels string `json:"dante_tx_channels,omitempty"`
|
||||
MulticastGroups MulticastMembershipSet `json:"multicast_groups,omitempty"`
|
||||
ArtNetInputs ArtNetUniverseSet `json:"artnet_inputs,omitempty"`
|
||||
ArtNetOutputs ArtNetUniverseSet `json:"artnet_outputs,omitempty"`
|
||||
SACNInputs []SACNUniverse `json:"sacn_inputs,omitempty"`
|
||||
SACNOutputs SACNUniverseSet `json:"sacn_outputs,omitempty"`
|
||||
DanteTx []*DantePeer `json:"dante_tx,omitempty"`
|
||||
DanteRx []*DantePeer `json:"dante_rx,omitempty"`
|
||||
Unreachable bool `json:"unreachable,omitempty"`
|
||||
}
|
||||
return json.Marshal(nodeJSON{
|
||||
ID: n.ID,
|
||||
Names: n.Names,
|
||||
Interfaces: n.Interfaces,
|
||||
MACTableSize: n.MACTableSize(),
|
||||
PoEBudget: n.PoEBudget,
|
||||
IsDanteClockMaster: n.IsDanteClockMaster,
|
||||
DanteTxChannels: n.DanteTxChannels,
|
||||
MulticastGroups: n.MulticastGroups,
|
||||
ArtNetInputs: n.ArtNetInputs,
|
||||
ArtNetOutputs: n.ArtNetOutputs,
|
||||
SACNInputs: n.SACNInputs(),
|
||||
SACNOutputs: n.SACNOutputs,
|
||||
DanteTx: n.DanteTx,
|
||||
DanteRx: n.DanteRx,
|
||||
Unreachable: n.Unreachable,
|
||||
})
|
||||
}
|
||||
|
||||
type DantePeer struct {
|
||||
Node *Node `json:"node"`
|
||||
Channels []string `json:"channels,omitempty"`
|
||||
@@ -170,7 +469,7 @@ func (p *DantePeer) MarshalJSON() ([]byte, error) {
|
||||
Status map[string]string `json:"status,omitempty"`
|
||||
}
|
||||
nodeRef := &Node{
|
||||
TypeID: p.Node.TypeID,
|
||||
ID: p.Node.ID,
|
||||
Names: p.Node.Names,
|
||||
Interfaces: p.Node.Interfaces,
|
||||
}
|
||||
@@ -190,10 +489,10 @@ func (n *Node) WithInterface(ifaceKey string) *Node {
|
||||
return n
|
||||
}
|
||||
return &Node{
|
||||
TypeID: n.TypeID,
|
||||
ID: n.ID,
|
||||
Names: n.Names,
|
||||
Interfaces: InterfaceMap{ifaceKey: iface},
|
||||
MACTableSize: n.MACTableSize,
|
||||
MACTable: n.MACTable,
|
||||
PoEBudget: n.PoEBudget,
|
||||
IsDanteClockMaster: n.IsDanteClockMaster,
|
||||
DanteTxChannels: n.DanteTxChannels,
|
||||
|
||||
Reference in New Issue
Block a user