diff --git a/cmd/wg-mesh/main.go b/cmd/wg-mesh/main.go index cfe56d2..7f879f7 100644 --- a/cmd/wg-mesh/main.go +++ b/cmd/wg-mesh/main.go @@ -6,6 +6,8 @@ import ( "os" "github.com/akamensky/argparse" + "github.com/tim-beatham/wgmesh/pkg/ctrlserver" + graph "github.com/tim-beatham/wgmesh/pkg/dot" "github.com/tim-beatham/wgmesh/pkg/ipc" logging "github.com/tim-beatham/wgmesh/pkg/log" ) @@ -91,17 +93,40 @@ func leaveMesh(client *ipcRpc.Client, meshId string) { fmt.Println(reply) } -func getGraph(client *ipcRpc.Client, meshId string) { - var reply string +func getGraph(client *ipcRpc.Client) { + listMeshesReply := new(ipc.ListMeshReply) - err := client.Call("IpcHandler.GetDOT", &meshId, &reply) + err := client.Call("IpcHandler.ListMeshes", "", &listMeshesReply) if err != nil { fmt.Println(err.Error()) return } - fmt.Println(reply) + meshes := make(map[string][]ctrlserver.MeshNode) + + for _, meshId := range listMeshesReply.Meshes { + var meshReply ipc.GetMeshReply + + err := client.Call("IpcHandler.GetMesh", &meshId, &meshReply) + + if err != nil { + fmt.Println(err.Error()) + return + } + + meshes[meshId] = meshReply.Nodes + } + + dotGenerator := graph.NewMeshGraphConverter(meshes) + dot, err := dotGenerator.Generate() + + if err != nil { + fmt.Println(err.Error()) + return + } + + fmt.Println(dot) } func queryMesh(client *ipcRpc.Client, meshId, query string) { @@ -258,11 +283,6 @@ func main() { Help: "Advertise ::/0 into the mesh network", }) - var getGraphMeshId *string = getGraphCmd.String("m", "mesh", &argparse.Options{ - Required: true, - Help: "MeshID of the graph to get", - }) - var leaveMeshMeshId *string = leaveMeshCmd.String("m", "mesh", &argparse.Options{ Required: true, Help: "MeshID of the mesh to leave", @@ -351,7 +371,7 @@ func main() { } if getGraphCmd.Happened() { - getGraph(client, *getGraphMeshId) + getGraph(client) } if leaveMeshCmd.Happened() { diff --git a/pkg/crdt/two_phase_map_test.go b/pkg/crdt/two_phase_map_test.go new file mode 100644 index 0000000..2d7fd1e --- /dev/null +++ b/pkg/crdt/two_phase_map_test.go @@ -0,0 +1,214 @@ +package crdt + +import ( + "hash/fnv" + "slices" + "testing" +) + +func NewMap(processId string) *TwoPhaseMap[string, string] { + theMap := NewTwoPhaseMap[string, string](processId, func(key string) uint64 { + hash := fnv.New64a() + hash.Write([]byte(key)) + return hash.Sum64() + }, 1) + return theMap +} + +func TestTwoPhaseMapEmpty(t *testing.T) { + theMap := NewMap("a") + + if theMap.Contains("a") { + t.Fatalf(`a should not be present in the map`) + } +} + +func TestTwoPhaseMapValuePresent(t *testing.T) { + theMap := NewMap("a") + theMap.Put("a", "") + + if !theMap.Contains("a") { + t.Fatalf(`should be present within the map`) + } +} + +func TestTwoPhaseMapValueNotPresent(t *testing.T) { + theMap := NewMap("a") + theMap.Put("b", "") + + if theMap.Contains("a") { + t.Fatalf(`a should not be present in the map`) + } +} + +func TestTwoPhaseMapPutThenRemove(t *testing.T) { + theMap := NewMap("a") + + theMap.Put("a", "") + theMap.Remove("a") + + if theMap.Contains("a") { + t.Fatalf(`a should not be present within the map`) + } +} + +func TestTwoPhaseMapPutThenRemoveThenPut(t *testing.T) { + theMap := NewMap("a") + + theMap.Put("a", "") + theMap.Remove("a") + theMap.Put("a", "") + + if !theMap.Contains("a") { + t.Fatalf(`a should be present within the map`) + } +} + +func TestMarkMarksTheValueIn2PMap(t *testing.T) { + theMap := NewMap("a") + + theMap.Put("a", "") + theMap.Mark("a") + + if !theMap.IsMarked("a") { + t.Fatalf(`a should be marked`) + } +} + +func TestAsListReturnsItemsInList(t *testing.T) { + theMap := NewMap("a") + + theMap.Put("a", "bob") + theMap.Put("b", "dylan") + + keys := theMap.AsList() + slices.Sort(keys) + + if !slices.Equal([]string{"bob", "dylan"}, keys) { + t.Fatalf(`values should be bob, dylan`) + } +} + +func TestSnapShotRemoveMapEmpty(t *testing.T) { + theMap := NewMap("a") + theMap.Put("a", "bob") + theMap.Put("b", "dylan") + + snapshot := theMap.Snapshot() + + if len(snapshot.Add) != 2 { + t.Fatalf(`add values length should be 2`) + } + + if len(snapshot.Remove) != 0 { + t.Fatalf(`remove map length should be 0`) + } +} + +func TestSnapshotMapEmpty(t *testing.T) { + theMap := NewMap("a") + + snapshot := theMap.Snapshot() + + if len(snapshot.Add) != 0 || len(snapshot.Remove) != 0 { + t.Fatalf(`snapshot length should be 0`) + } +} + +func TestSnapShotFromStateReturnsIntersection(t *testing.T) { + map1 := NewMap("a") + map1.Put("a", "heyy") + + map2 := NewMap("b") + map2.Put("b", "hmmm") + + message := map2.GenerateMessage() + + snapShot := map1.SnapShotFromState(message) + + if len(snapShot.Add) != 1 { + t.Fatalf(`add length should be 1`) + } + + if len(snapShot.Remove) != 0 { + t.Fatalf(`remove length should be 0`) + } +} + +func TestGetHashDifferentOnChange(t *testing.T) { + theMap := NewMap("a") + + prevHash := theMap.GetHash() + + theMap.Put("b", "hmmhmhmh") + + if prevHash == theMap.GetHash() { + t.Fatalf(`hashes should not be the same`) + } +} + +func TestGenerateMessageReturnsClocks(t *testing.T) { + theMap := NewMap("a") + theMap.Put("a", "hmm") + theMap.Put("b", "hmm") + theMap.Remove("a") + + message := theMap.GenerateMessage() + + if len(message.AddContents) != 2 { + t.Fatalf(`two items added add should be 2`) + } + + if len(message.RemoveContents) != 1 { + t.Fatalf(`a was removed remove map should be length 1`) + } +} + +func TestDifferenceReturnsDifferenceOfMaps(t *testing.T) { + map1 := NewMap("a") + map1.Put("a", "ssms") + map1.Put("b", "sdmdsmd") + + map2 := NewMap("b") + map2.Put("d", "eek") + map2.Put("c", "meh") + + message1 := map1.GenerateMessage() + message2 := map2.GenerateMessage() + + difference := message1.Difference(0, message2) + + if len(difference.AddContents) != 2 { + t.Fatalf(`d and c are not in map1 they should be in add contents`) + } + + if len(difference.RemoveContents) != 0 { + t.Fatalf(`remove should be empty`) + } +} + +func TestMergeMergesValuesThatAreGreaterThanCurrentClock(t *testing.T) { + map1 := NewMap("a") + map1.Put("a", "ssms") + map1.Put("b", "sdmdsmd") + + map2 := NewMap("b") + map2.Put("d", "eek") + map2.Put("c", "meh") + + message1 := map1.GenerateMessage() + message2 := map2.GenerateMessage() + + difference := message1.Difference(0, message2) + state := map2.SnapShotFromState(difference) + + map1.Merge(*state) + + if !map1.Contains("d") { + t.Fatalf(`d should be in the map`) + } + + if !map2.Contains("c") { + t.Fatalf(`c should be in the map`) + } +} diff --git a/pkg/dot/dot.go b/pkg/dot/dot.go new file mode 100644 index 0000000..8df6173 --- /dev/null +++ b/pkg/dot/dot.go @@ -0,0 +1,227 @@ +// Graph allows the definition of a DOT graph in golang +package graph + +import ( + "fmt" + "hash/fnv" + "strings" + + "github.com/tim-beatham/wgmesh/pkg/lib" +) + +type GraphType string +type Shape string + +const ( + GRAPH GraphType = "graph" + DIGRAPH GraphType = "digraph" +) + +const ( + CIRCLE Shape = "circle" + STAR Shape = "star" + HEXAGON Shape = "hexagon" + PARALLELOGRAM Shape = "parallelogram" +) + +type Graph interface { + Dottable + GetType() GraphType +} + +type Cluster struct { + Type GraphType + Name string + Label string + nodes map[string]*Node + edges map[string]Edge +} + +type RootGraph struct { + Type GraphType + Label string + nodes map[string]*Node + clusters map[string]*Cluster + edges map[string]Edge +} + +type Node struct { + Name string + Label string + Shape Shape + Size int +} + +type Edge interface { + Dottable +} + +type DirectedEdge struct { + Name string + Label string + From string + To string +} + +type UndirectedEdge struct { + Name string + Label string + From string + To string +} + +// Dottable means an implementer can convert the struct to DOT representation +type Dottable interface { + GetDOT() (string, error) +} + +func NewGraph(label string, graphType GraphType) *RootGraph { + return &RootGraph{Type: graphType, Label: label, clusters: map[string]*Cluster{}, nodes: make(map[string]*Node), edges: make(map[string]Edge)} +} + +// PutNode: puts a node in the graph +func (g *RootGraph) PutNode(name, label string, size int, shape Shape) error { + _, exists := g.nodes[name] + + if exists { + // If exists no need to add the ndoe + return nil + } + + g.nodes[name] = &Node{Name: name, Label: label, Size: size, Shape: shape} + return nil +} + +func (g *RootGraph) PutCluster(graph *Cluster) { + g.clusters[graph.Label] = graph +} + +func writeContituents[D Dottable](result *strings.Builder, elements ...D) error { + for _, node := range elements { + dot, err := node.GetDOT() + + if err != nil { + return err + } + + _, err = result.WriteString(dot) + + if err != nil { + return err + } + } + return nil +} + +func (g *RootGraph) GetDOT() (string, error) { + var result strings.Builder + + result.WriteString(fmt.Sprintf("%s {\n", g.Type)) + result.WriteString("node [colorscheme=set312];\n") + result.WriteString("layout = fdp;\n") + nodes := lib.MapValues(g.nodes) + edges := lib.MapValues(g.edges) + writeContituents(&result, nodes...) + writeContituents(&result, edges...) + + for _, cluster := range g.clusters { + clusterDOT, err := cluster.GetDOT() + + if err != nil { + return "", err + } + + result.WriteString(clusterDOT) + } + + result.WriteString("}") + return result.String(), nil +} + +// GetType implements Graph. +func (r *RootGraph) GetType() GraphType { + return r.Type +} + +func constructEdge(graph Graph, name, label, from, to string) Edge { + switch graph.GetType() { + case DIGRAPH: + return &DirectedEdge{Name: name, Label: label, From: from, To: to} + default: + return &UndirectedEdge{Name: name, Label: label, From: from, To: to} + } +} + +// AddEdge: adds an edge between two nodes in the graph +func (g *RootGraph) AddEdge(name string, label string, from string, to string) error { + g.edges[name] = constructEdge(g, name, label, from, to) + return nil +} + +const numColours = 12 + +func (n *Node) hash() int { + h := fnv.New32a() + h.Write([]byte(n.Name)) + return (int(h.Sum32()) % numColours) + 1 +} + +func (n *Node) GetDOT() (string, error) { + return fmt.Sprintf("node[label=\"%s\",shape=%s, style=\"filled\", fillcolor=%d, width=%d, height=%d, fixedsize=true] \"%s\";\n", + n.Label, n.Shape, n.hash(), n.Size, n.Size, n.Name), nil +} + +func (e *DirectedEdge) GetDOT() (string, error) { + return fmt.Sprintf("\"%s\" -> \"%s\" [label=\"%s\"];\n", e.From, e.To, e.Label), nil +} + +func (e *UndirectedEdge) GetDOT() (string, error) { + return fmt.Sprintf("\"%s\" -- \"%s\" [label=\"%s\"];\n", e.From, e.To, e.Label), nil +} + +// AddEdge: adds an edge between two nodes in the graph +func (g *Cluster) AddEdge(name string, label string, from string, to string) error { + g.edges[name] = constructEdge(g, name, label, from, to) + return nil +} + +// PutNode: puts a node in the graph +func (g *Cluster) PutNode(name, label string, size int, shape Shape) error { + _, exists := g.nodes[name] + + if exists { + // If exists no need to add the ndoe + return nil + } + + g.nodes[name] = &Node{Name: name, Label: label, Shape: shape, Size: size} + return nil +} + +func (g *Cluster) GetDOT() (string, error) { + var builder strings.Builder + + builder.WriteString(fmt.Sprintf("subgraph \"cluster%s\" {\n", g.Label)) + builder.WriteString(fmt.Sprintf("label = \"%s\"\n", g.Label)) + nodes := lib.MapValues(g.nodes) + edges := lib.MapValues(g.edges) + writeContituents(&builder, nodes...) + writeContituents(&builder, edges...) + + builder.WriteString("}\n") + return builder.String(), nil +} + +func (g *Cluster) GetType() GraphType { + return g.Type +} + +func NewSubGraph(name string, label string, graphType GraphType) *Cluster { + return &Cluster{ + Label: name, + Type: graphType, + Name: name, + nodes: make(map[string]*Node), + edges: make(map[string]Edge), + } +} diff --git a/pkg/dot/wg.go b/pkg/dot/wg.go new file mode 100644 index 0000000..d592047 --- /dev/null +++ b/pkg/dot/wg.go @@ -0,0 +1,116 @@ +package graph + +import ( + "fmt" + "slices" + + "github.com/tim-beatham/wgmesh/pkg/ctrlserver" +) + +// MeshGraphConverter converts a mesh to a graph +type MeshGraphConverter interface { + // convert the mesh to textual form + Generate() (string, error) +} + +type MeshDOTConverter struct { + meshes map[string][]ctrlserver.MeshNode + destinations map[string]interface{} +} + +func (c *MeshDOTConverter) Generate() (string, error) { + g := NewGraph("Smegmesh", GRAPH) + + for meshId := range c.meshes { + err := c.generateMesh(g, meshId) + + if err != nil { + return "", err + } + } + + for mesh := range c.meshes { + g.PutNode(mesh, mesh, 1, CIRCLE) + } + + for destination := range c.destinations { + g.PutNode(destination, destination, 1, HEXAGON) + } + + return g.GetDOT() +} + +func (c *MeshDOTConverter) generateMesh(g *RootGraph, meshId string) error { + nodes := c.meshes[meshId] + + g.PutNode(meshId, meshId, 1, CIRCLE) + + for _, node := range nodes { + c.graphNode(g, node, meshId) + } + + for _, node := range nodes { + g.AddEdge(fmt.Sprintf("%s to %s", node.PublicKey, meshId), "", node.PublicKey, meshId) + } + + return nil +} + +// graphNode: graphs a node within the mesh +func (c *MeshDOTConverter) graphNode(g *RootGraph, node ctrlserver.MeshNode, meshId string) { + alias := node.Alias + + if alias == "" { + alias = node.WgHost[1:len(node.WgHost)-20] + "\\n" + node.WgHost[len(node.WgHost)-20:len(node.WgHost)] + } + + g.PutNode(node.PublicKey, alias, 2, CIRCLE) + + for _, route := range node.Routes { + if len(route.Path) == 0 { + g.AddEdge(route.Destination, "", node.PublicKey, route.Destination) + continue + } + + reversedPath := slices.Clone(route.Path) + slices.Reverse(reversedPath) + + g.AddEdge(fmt.Sprintf("%s to %s", node.PublicKey, reversedPath[0]), "", node.PublicKey, reversedPath[0]) + + for _, mesh := range route.Path { + if _, ok := c.meshes[mesh]; !ok { + c.destinations[mesh] = struct{}{} + } + } + + for index := range reversedPath[0 : len(reversedPath)-1] { + routeID := fmt.Sprintf("%s to %s", reversedPath[index], reversedPath[index+1]) + g.AddEdge(routeID, "", reversedPath[index], reversedPath[index+1]) + } + + if route.Destination == "::/0" { + c.destinations[route.Destination] = struct{}{} + lastMesh := reversedPath[len(reversedPath)-1] + routeID := fmt.Sprintf("%s to %s", lastMesh, route.Destination) + g.AddEdge(routeID, "", lastMesh, route.Destination) + } + } + + for service := range node.Services { + c.putService(g, service, meshId, node) + } +} + +// putService: construct a service node and a link between the nodes +func (c *MeshDOTConverter) putService(g *RootGraph, key, meshId string, node ctrlserver.MeshNode) { + serviceID := fmt.Sprintf("%s%s%s", key, node.PublicKey, meshId) + g.PutNode(serviceID, key, 1, PARALLELOGRAM) + g.AddEdge(fmt.Sprintf("%s to %s", node.PublicKey, serviceID), "", node.PublicKey, serviceID) +} + +func NewMeshGraphConverter(meshes map[string][]ctrlserver.MeshNode) MeshGraphConverter { + return &MeshDOTConverter{ + meshes: meshes, + destinations: make(map[string]interface{}), + } +} diff --git a/pkg/graph/graph.go b/pkg/graph/graph.go deleted file mode 100644 index 1f0d4a4..0000000 --- a/pkg/graph/graph.go +++ /dev/null @@ -1,178 +0,0 @@ -// Graph allows the definition of a DOT graph in golang -package graph - -import ( - "errors" - "fmt" - "hash/fnv" - "strings" - - "github.com/tim-beatham/wgmesh/pkg/lib" -) - -type GraphType string -type Shape string - -const ( - GRAPH GraphType = "graph" - DIGRAPH = "digraph" -) - -const ( - CIRCLE Shape = "circle" - STAR Shape = "star" - HEXAGON Shape = "hexagon" -) - -type Graph struct { - Type GraphType - Label string - nodes map[string]*Node - edges []Edge -} - -type Node struct { - Name string - Shape Shape -} - -type Edge interface { - Dottable -} - -type DirectedEdge struct { - Label string - From *Node - To *Node -} - -type UndirectedEdge struct { - Label string - From *Node - To *Node -} - -// Dottable means an implementer can convert the struct to DOT representation -type Dottable interface { - GetDOT() (string, error) -} - -func NewGraph(label string, graphType GraphType) *Graph { - return &Graph{Type: graphType, Label: label, nodes: make(map[string]*Node), edges: make([]Edge, 0)} -} - -// PutNode: puts a node in the graph -func (g *Graph) PutNode(label string, shape Shape) error { - _, exists := g.nodes[label] - - if exists { - // If exists no need to add the ndoe - return nil - } - - g.nodes[label] = &Node{Name: label, Shape: shape} - return nil -} - -func writeContituents[D Dottable](result *strings.Builder, elements ...D) error { - for _, node := range elements { - dot, err := node.GetDOT() - - if err != nil { - return err - } - - _, err = result.WriteString(dot) - - if err != nil { - return err - } - } - return nil -} - -func (g *Graph) GetDOT() (string, error) { - var result strings.Builder - - _, err := result.WriteString(fmt.Sprintf("%s {\n", g.Type)) - - if err != nil { - return "", err - } - - _, err = result.WriteString("node [colorscheme=set312];\n") - - if err != nil { - return "", err - } - - nodes := lib.MapValues(g.nodes) - - err = writeContituents(&result, nodes...) - - if err != nil { - return "", err - } - - err = writeContituents(&result, g.edges...) - - if err != nil { - return "", err - } - - _, err = result.WriteString("}") - - if err != nil { - return "", err - } - - return result.String(), nil -} - -func (g *Graph) constructEdge(label string, from *Node, to *Node) Edge { - switch g.Type { - case DIGRAPH: - return &DirectedEdge{Label: label, From: from, To: to} - default: - return &UndirectedEdge{Label: label, From: from, To: to} - } -} - -// AddEdge: adds an edge between two nodes in the graph -func (g *Graph) AddEdge(label string, from string, to string) error { - fromNode, exists := g.nodes[from] - - if !exists { - return errors.New(fmt.Sprintf("Node %s does not exist", from)) - } - - toNode, exists := g.nodes[to] - - if !exists { - return errors.New(fmt.Sprintf("Node %s does not exist", to)) - } - - g.edges = append(g.edges, g.constructEdge(label, fromNode, toNode)) - return nil -} - -const numColours = 12 - -func (n *Node) hash() int { - h := fnv.New32a() - h.Write([]byte(n.Name)) - return (int(h.Sum32()) % numColours) + 1 -} - -func (n *Node) GetDOT() (string, error) { - return fmt.Sprintf("node[shape=%s, style=\"filled\", fillcolor=%d] %s;\n", - n.Shape, n.hash(), n.Name), nil -} - -func (e *DirectedEdge) GetDOT() (string, error) { - return fmt.Sprintf("%s -> %s;\n", e.From.Name, e.To.Name), nil -} - -func (e *UndirectedEdge) GetDOT() (string, error) { - return fmt.Sprintf("%s -- %s;\n", e.From.Name, e.To.Name), nil -} diff --git a/pkg/ipc/ipc.go b/pkg/ipc/ipc.go index 8496578..b8896ee 100644 --- a/pkg/ipc/ipc.go +++ b/pkg/ipc/ipc.go @@ -68,7 +68,6 @@ type MeshIpc interface { JoinMesh(args JoinMeshArgs, reply *string) error LeaveMesh(meshId string, reply *string) error GetMesh(meshId string, reply *GetMeshReply) error - GetDOT(meshId string, reply *string) error Query(query QueryMesh, reply *string) error PutDescription(description string, reply *string) error PutAlias(alias string, reply *string) error diff --git a/pkg/mesh/graph.go b/pkg/mesh/graph.go deleted file mode 100644 index 33891f3..0000000 --- a/pkg/mesh/graph.go +++ /dev/null @@ -1,77 +0,0 @@ -package mesh - -import ( - "errors" - "fmt" - - "github.com/tim-beatham/wgmesh/pkg/graph" - "github.com/tim-beatham/wgmesh/pkg/lib" -) - -// MeshGraphConverter converts a mesh to a graph -type MeshGraphConverter interface { - // convert the mesh to textual form - Generate(meshId string) (string, error) -} - -type MeshDOTConverter struct { - manager MeshManager -} - -func (c *MeshDOTConverter) Generate(meshId string) (string, error) { - mesh := c.manager.GetMesh(meshId) - - if mesh == nil { - return "", errors.New("mesh does not exist") - } - - g := graph.NewGraph(meshId, graph.GRAPH) - - snapshot, err := mesh.GetMesh() - - if err != nil { - return "", err - } - - for _, node := range snapshot.GetNodes() { - c.graphNode(g, node, meshId) - } - - nodes := lib.MapValues(snapshot.GetNodes()) - - for i, node1 := range nodes[:len(nodes)-1] { - for _, node2 := range nodes[i+1:] { - if node1.GetWgEndpoint() == node2.GetWgEndpoint() { - continue - } - - node1Id := fmt.Sprintf("\"%s\"", node1.GetIdentifier()) - node2Id := fmt.Sprintf("\"%s\"", node2.GetIdentifier()) - g.AddEdge(fmt.Sprintf("%s to %s", node1Id, node2Id), node1Id, node2Id) - } - } - - return g.GetDOT() -} - -// graphNode: graphs a node within the mesh -func (c *MeshDOTConverter) graphNode(g *graph.Graph, node MeshNode, meshId string) { - nodeId := fmt.Sprintf("\"%s\"", node.GetIdentifier()) - g.PutNode(nodeId, graph.CIRCLE) - - self, _ := c.manager.GetSelf(meshId) - - if NodeEquals(self, node) { - return - } - - for _, route := range node.GetRoutes() { - routeId := fmt.Sprintf("\"%s\"", route) - g.PutNode(routeId, graph.HEXAGON) - g.AddEdge(fmt.Sprintf("%s to %s", nodeId, routeId), nodeId, routeId) - } -} - -func NewMeshDotConverter(m MeshManager) MeshGraphConverter { - return &MeshDOTConverter{manager: m} -} diff --git a/pkg/robin/requester.go b/pkg/robin/requester.go index 75ef61e..81bb82b 100644 --- a/pkg/robin/requester.go +++ b/pkg/robin/requester.go @@ -182,19 +182,6 @@ func (n *IpcHandler) GetMesh(meshId string, reply *ipc.GetMeshReply) error { return nil } -func (n *IpcHandler) GetDOT(meshId string, reply *string) error { - g := mesh.NewMeshDotConverter(n.Server.GetMeshManager()) - - result, err := g.Generate(meshId) - - if err != nil { - return err - } - - *reply = result - return nil -} - func (n *IpcHandler) Query(params ipc.QueryMesh, reply *string) error { queryResponse, err := n.Server.GetQuerier().Query(params.MeshId, params.Query)