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?