From 1ba6be2957eab5b49190ed56027a20d151d60ba5 Mon Sep 17 00:00:00 2001 From: James Shubin Date: Tue, 29 Dec 2015 01:04:03 -0500 Subject: [PATCH] Add graphviz generation and visualization This requires graphviz to be installed on your machine. If you run the command with sudo, it will create the files with the original user ownership to make it easier to remove them without root. --- file.go | 4 +++ main.go | 16 ++++++++++ pgraph.go | 94 +++++++++++++++++++++++++++++++++++++++++++++++++++++- service.go | 4 +++ types.go | 9 ++++++ 5 files changed, 126 insertions(+), 1 deletion(-) diff --git a/file.go b/file.go index ce7423ae..0cdc5497 100644 --- a/file.go +++ b/file.go @@ -55,6 +55,10 @@ func NewFileType(name, path, content, state string) *FileType { } } +func (obj *FileType) GetType() string { + return "File" +} + // File watcher for files and directories // Modify with caution, probably important to write some test cases first! // obj.Path: file or directory diff --git a/main.go b/main.go index 7046f690..88ad2cee 100644 --- a/main.go +++ b/main.go @@ -102,6 +102,12 @@ func run(c *cli.Context) { log.Fatal("Graph failure") } log.Printf("Graph: %v\n", G) // show graph + err := G.ExecGraphviz(c.String("graphviz-filter"), c.String("graphviz")) + if err != nil { + log.Printf("Graphviz: %v", err) + } else { + log.Printf("Graphviz: Successfully generated graph!") + } G.SetVertex() if first { // G.Start(...) needs to be synchronous or wait, @@ -174,6 +180,16 @@ func main() { Value: "", Usage: "code definition to run", }, + cli.StringFlag{ + Name: "graphviz, g", + Value: "", + Usage: "output file for graphviz data", + }, + cli.StringFlag{ + Name: "graphviz-filter, gf", + Value: "dot", // directed graph default + Usage: "graphviz filter to use", + }, // useful for testing multiple instances on same machine cli.StringFlag{ Name: "hostname", diff --git a/pgraph.go b/pgraph.go index 7862671f..31c5217e 100644 --- a/pgraph.go +++ b/pgraph.go @@ -19,10 +19,15 @@ package main import ( - //"container/list" // doubly linked list + "errors" "fmt" + "io/ioutil" "log" + "os" + "os/exec" + "strconv" "sync" + "syscall" ) //go:generate stringer -type=graphState -output=graphstate_stringer.go @@ -81,6 +86,11 @@ func NewEdge(name string) *Edge { } } +// returns the name of the graph +func (g *Graph) GetName() string { + return g.Name +} + // set name of the graph func (g *Graph) SetName(name string) { g.Name = name @@ -208,6 +218,88 @@ func (g *Graph) String() string { return fmt.Sprintf("Vertices(%d), Edges(%d)", g.NumVertices(), g.NumEdges()) } +// output the graph in graphviz format +// https://en.wikipedia.org/wiki/DOT_%28graph_description_language%29 +func (g *Graph) Graphviz() (out string) { + //digraph g { + // label="hello world"; + // node [shape=box]; + // A [label="A"]; + // B [label="B"]; + // C [label="C"]; + // D [label="D"]; + // E [label="E"]; + // A -> B [label=f]; + // B -> C [label=g]; + // D -> E [label=h]; + //} + out += fmt.Sprintf("digraph %v {\n", g.GetName()) + out += fmt.Sprintf("\tlabel=\"%v\";\n", g.GetName()) + //out += "\tnode [shape=box];\n" + str := "" + for i, _ := range g.Adjacency { // reverse paths + out += fmt.Sprintf("\t%v [label=\"%v[%v]\"];\n", i.GetName(), i.GetType(), i.GetName()) + for j, _ := range g.Adjacency[i] { + k := g.Adjacency[i][j] + // use str for clearer output ordering + str += fmt.Sprintf("\t%v -> %v [label=%v];\n", i.GetName(), j.GetName(), k.Name) + } + } + out += str + out += "}\n" + return +} + +// write out the graphviz data and run the correct graphviz filter command +func (g *Graph) ExecGraphviz(program, filename string) error { + + switch program { + case "dot", "neato", "twopi", "circo", "fdp": + default: + return errors.New("Invalid graphviz program selected!") + } + + if filename == "" { + return errors.New("No filename given!") + } + + // run as a normal user if possible when run with sudo + uid, err1 := strconv.Atoi(os.Getenv("SUDO_UID")) + gid, err2 := strconv.Atoi(os.Getenv("SUDO_GID")) + + err := ioutil.WriteFile(filename, []byte(g.Graphviz()), 0644) + if err != nil { + return errors.New("Error writing to filename!") + } + + if err1 == nil && err2 == nil { + if err := os.Chown(filename, uid, gid); err != nil { + return errors.New("Error changing file owner!") + } + } + + path, err := exec.LookPath(program) + if err != nil { + return errors.New("Graphviz is missing!") + } + + out := fmt.Sprintf("%v.png", filename) + cmd := exec.Command(path, "-Tpng", fmt.Sprintf("-o%v", out), filename) + + if err1 == nil && err2 == nil { + cmd.SysProcAttr = &syscall.SysProcAttr{} + cmd.SysProcAttr.Credential = &syscall.Credential{ + Uid: uint32(uid), + Gid: uint32(gid), + } + } + _, err = cmd.Output() + if err != nil { + return errors.New("Error writing to image!") + } + return nil +} + // google/golang hackers apparently do not think contains should be a built-in! func Contains(s []*Vertex, element *Vertex) bool { for _, v := range s { diff --git a/service.go b/service.go index 76319b5b..1576143a 100644 --- a/service.go +++ b/service.go @@ -45,6 +45,10 @@ func NewServiceType(name, state, startup string) *ServiceType { } } +func (obj *ServiceType) GetType() string { + return "Service" +} + // Service watcher func (obj *ServiceType) Watch() { // obj.Name: service name diff --git a/types.go b/types.go index addac288..5cf33529 100644 --- a/types.go +++ b/types.go @@ -26,6 +26,7 @@ import ( type Type interface { Init() GetName() string // can't be named "Name()" because of struct field + GetType() string Watch() StateOK() bool // TODO: can we rename this to something better? Apply() bool @@ -71,6 +72,10 @@ func (obj *BaseType) GetName() string { return obj.Name } +func (obj *BaseType) GetType() string { + return "Base" +} + func (obj *BaseType) GetVertex() *Vertex { return obj.vertex } @@ -198,6 +203,10 @@ func (obj *BaseType) Process(typ Type) { } +func (obj *NoopType) GetType() string { + return "Noop" +} + func (obj *NoopType) Watch() { //vertex := obj.vertex // stored with SetVertex var send = false // send event?