Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 18 additions & 0 deletions config.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,3 +21,21 @@ func options(name string) bool {
trace("Option [" + name + "] was not found - returning the default: false")
return false
}

// optionBoolDefault returns a boolean from options, or ifMissing if the key is not set.
func optionBoolDefault(name string, ifMissing bool) bool {
if g_config != nil && len(name) > 0 {
if copts, found := g_config["options"]; found {
opts, found := copts.(map[string]interface{})
if found && len(opts) > 0 {
if value, found := opts[name]; found {
if b, ok := value.(bool); ok {
return b
}
}
}
}
}
trace("Option [" + name + "] was not found - returning the default: ", ifMissing)
return ifMissing
}
3 changes: 2 additions & 1 deletion config.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
"orientation": "LR",
"node.shape": "plaintext",
"node.font.size": "10",
"node.font.name": "Ubuntu",
"node.font.name": "Arial",

"text.align.header": "right",
"text.align.sequence": "right",
Expand Down Expand Up @@ -98,6 +98,7 @@
},
"options" : {
"allow missing imports": true,
"allow unresolved types": true,
"show missing types": true,
"generate .png file": false,
"generate .svg file": true,
Expand Down
32 changes: 28 additions & 4 deletions graphviz.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,26 @@ import (
"github.com/seamia/tools/support"
)

func graphvizStderr(err error) string {
if ee, ok := err.(*exec.ExitError); ok && len(ee.Stderr) > 0 {
return string(ee.Stderr)
}
return ""
}

// closeDotAndRunGraphviz closes the .dot file handle, then runs graphviz if the file exists.
// Closing before invoking dot is required on Windows so the file is not locked.
func closeDotAndRunGraphviz(pbs *pbstate) {
if pbs == nil || pbs.outputFile == "" {
return
}
pbs.closeOutputWriters()
if _, err := os.Stat(pbs.outputFile); err != nil {
return
}
graphviz(pbs.outputFile, options(generateSvg), options(generatePng))
}

// (optionally) running 'graphviz' on the given .dot file
func graphviz(src string, svg, png bool) {

Expand All @@ -29,26 +49,30 @@ func graphviz(src string, svg, png bool) {
if graphviz, err := support.GetLocation(g_config, "graphviz"); err == nil && len(graphviz) > 0 {
if svg {
status("generating .svg file")
if output, e := exec.Command(graphviz, "-Tsvg", src).Output(); e == nil {
cmd := exec.Command(graphviz, "-Tsvg", src)
// Use Output (stdout only). CombinedOutput would prepend Pango/font warnings
// from stderr and produce invalid XML in the .svg file.
if output, e := cmd.Output(); e == nil {
if err := os.WriteFile(svgPath, output, 0755); err != nil {
status("error on write", err)
svgPath = ""
}
} else {
status("error on exec", e)
status("error on exec", e, graphvizStderr(e))
svgPath = ""
}
}

if png {
status("generating .png file")
if output, e := exec.Command(graphviz, "-Tpng", src).Output(); e == nil {
cmd := exec.Command(graphviz, "-Tpng", src)
if output, e := cmd.Output(); e == nil {
if err := os.WriteFile(pngPath, output, 0755); err != nil {
status("error on write", err)
pngPath = ""
}
} else {
status("error on exec", e)
status("error on exec", e, graphvizStderr(e))
pngPath = ""
}
}
Expand Down
30 changes: 30 additions & 0 deletions io.go
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,20 @@ func (cow *CreateOnWrite) Write(p []byte) (n int, err error) {
return cow.writer.Write(p)
}

// Close releases the open .dot file handle. Required on Windows before another process
// (e.g. graphviz) can read the file; otherwise opening the same path may fail with "Permission denied".
func (cow *CreateOnWrite) Close() error {
if cow == nil || cow.writer == nil {
return nil
}
var err error
if c, ok := cow.writer.(io.Closer); ok {
err = c.Close()
}
cow.writer = nil
return err
}

// ----------------------------------------------------------------------------------------------------------------------
type ForkWriter struct {
writers []io.Writer
Expand All @@ -63,6 +77,22 @@ func (fw *ForkWriter) Write(p []byte) (n int, err error) {
return
}

// Close closes all underlying writers that implement io.Closer (e.g. CreateOnWrite).
func (fw *ForkWriter) Close() error {
if fw == nil {
return nil
}
var first error
for _, w := range fw.writers {
if c, ok := w.(io.Closer); ok {
if err := c.Close(); err != nil && first == nil {
first = err
}
}
}
return first
}

func createDirIfMissing(name string) {
expanded := os.ExpandEnv(name)
if len(expanded) > 0 {
Expand Down
71 changes: 56 additions & 15 deletions main.go
Original file line number Diff line number Diff line change
Expand Up @@ -144,6 +144,13 @@ func (pbs *pbstate) AddWriter(target io.Writer) {
pbs.writer.AddWriter(target)
}

func (pbs *pbstate) closeOutputWriters() {
if pbs == nil || pbs.writer == nil {
return
}
_ = pbs.writer.Close()
}

func (pbs *pbstate) target() io.Writer {
if pbs.writer != nil {
return pbs.writer
Expand Down Expand Up @@ -203,6 +210,9 @@ func (pbs *pbstate) getResolution(scope FullName, shorttype OriginalName) *tinfo
}

func (pbs *pbstate) recordInclusion(from UniqueName, field string, to UniqueName) {
if pbs.isUniqueNameMissingTypePlaceholder(to) && !pbs.showMissingTypeGraphElements() {
return
}

fullFrom := from
if len(field) > 0 {
Expand All @@ -215,6 +225,20 @@ func (pbs *pbstate) recordInclusion(from UniqueName, field string, to UniqueName
pbs.inclusions[fullFrom][to]++
}

// showMissingTypeGraphElements is true when placeholder nodes for unresolved types and edges to them are drawn.
func (pbs *pbstate) showMissingTypeGraphElements() bool {
return options("show missing types")
}

func (pbs *pbstate) isUniqueNameMissingTypePlaceholder(to UniqueName) bool {
full, ok := pbs.knownNames[to]
if !ok {
return false
}
info, ok := pbs.types237[full]
return ok && info.typename == typenameMissing
}

func renderMissingNode(name OriginalName, unique UniqueName, fullname FullName) string {

writer := bytes.NewBufferString("")
Expand Down Expand Up @@ -250,18 +274,29 @@ func (pbs *pbstate) recordMissingType(from UniqueName, missingType OriginalName)
}
}

func (pbs *pbstate) recordMissingInclusion(from UniqueName, field string, missingType OriginalName) {
func (pbs *pbstate) recordMissingInclusion(source FullName, from UniqueName, field string, missingType OriginalName) {
debug("****** Field [", field, "] from [", from, "] refers to non-existing type [", missingType, "] ******")

if options("show missing types") {
// 1. save type (if not already)
unique := pbs.recordMissingType(from, missingType)
if !optionBoolDefault("allow unresolved types", true) {
msg := fmt.Sprintf("unresolved type %q; source: %s (set options[\"allow unresolved types\"] to true to continue, or correct includes/imports)", missingType, source)
alert(msg)
panic(msg)
}

// 2. record the connection
// When allowed, register a placeholder so field resolution and getKind succeed; "show missing types" controls graph only.
unique := pbs.recordMissingType(from, missingType)
if pbs.showMissingTypeGraphElements() {
pbs.recordInclusion(from, field, unique)
}
}

func (pbs *pbstate) includeProtoNodeInGraphOutput(info tinfo) bool {
if info.typename != typenameMissing {
return true
}
return pbs.showMissingTypeGraphElements()
}

func (pbs *pbstate) getInclusion(from UniqueName, field string) (UniqueName, map[UniqueName]int) {

fullFrom := from
Expand Down Expand Up @@ -488,20 +523,26 @@ func (pbs *pbstate) showInclusion(groupByPackages bool, leaveRootPackageUnwrappe

pbs.applyTemplate("comment", "leaving the root package unwrapped")
for _, info := range members {
pbs.applyTemplate("entry", info.raw)
if pbs.includeProtoNodeInGraphOutput(info) {
pbs.applyTemplate("entry", info.raw)
}
}
} else {

pbs.applyTemplate("cluster.prefix", data)
for _, info := range members {
pbs.applyTemplate("cluster.entry", info.raw)
if pbs.includeProtoNodeInGraphOutput(info) {
pbs.applyTemplate("cluster.entry", info.raw)
}
}
pbs.applyTemplate("cluster.suffix", data)
}
}
} else {
for _, info := range pbs.types237 {
pbs.applyTemplate("entry", info.raw)
if pbs.includeProtoNodeInGraphOutput(info) {
pbs.applyTemplate("entry", info.raw)
}
}
}

Expand Down Expand Up @@ -892,7 +933,7 @@ func (pbs *pbstate) handleMessageBody(msg *proto.Message) {
pbs.encounteredType(info.unique, actual.Name, inf.unique)
} else {
alert("failed to resolve", actual.Type)
pbs.recordMissingInclusion(info.unique, actual.Name, OriginalName(actual.Type))
pbs.recordMissingInclusion(full, info.unique, actual.Name, OriginalName(actual.Type))
}
}

Expand Down Expand Up @@ -921,7 +962,7 @@ func (pbs *pbstate) handleMessageBody(msg *proto.Message) {
pbs.recordInclusion(info.unique, actual.Name, inf.unique)
} else {
alert("failed to resolve type [", actual.Type, "] from ", full)
pbs.recordMissingInclusion(info.unique, actual.Name, OriginalName(actual.Type))
pbs.recordMissingInclusion(full, info.unique, actual.Name, OriginalName(actual.Type))
}
}

Expand Down Expand Up @@ -957,7 +998,7 @@ func (pbs *pbstate) onOneof(fullname FullName, unique UniqueName, one *proto.One
pbs.encounteredType(unique, actual.Name, inf.unique)
} else {
alert("failed to get unique name for type", actual.Type)
pbs.recordMissingInclusion(unique, actual.Name, OriginalName(actual.Type))
pbs.recordMissingInclusion(fullname, unique, actual.Name, OriginalName(actual.Type))
}
}

Expand Down Expand Up @@ -1095,7 +1136,7 @@ func (pbs *pbstate) handleServiceBody(srv *proto.Service) {
pbs.recordInclusion(info.unique, field, inf.unique)
} else {
alert("failed to resolve type [", actual.RequestType, "] from ", full)
pbs.recordMissingInclusion(info.unique, field, OriginalName(actual.RequestType))
pbs.recordMissingInclusion(full, info.unique, field, OriginalName(actual.RequestType))
}
}

Expand All @@ -1106,7 +1147,7 @@ func (pbs *pbstate) handleServiceBody(srv *proto.Service) {
pbs.recordInclusion(info.unique, field, inf.unique)
} else {
alert("failed to resolve type [", actual.ReturnsType, "] from ", full)
pbs.recordMissingInclusion(info.unique, field, OriginalName(actual.ReturnsType))
pbs.recordMissingInclusion(full, info.unique, field, OriginalName(actual.ReturnsType))
}
}

Expand Down Expand Up @@ -1344,7 +1385,7 @@ func processOneProto(name, selection string) {

pbs := NewPbs()
process(pbs, name, selection)
graphviz(pbs.outputFile, options(generateSvg), options(generatePng))
closeDotAndRunGraphviz(pbs)
}

func applyToAllFiles(root, selection string) {
Expand Down Expand Up @@ -1492,6 +1533,6 @@ func main() {
} else {
pbs := NewPbs()
process(pbs, *g_source, *g_selection)
graphviz(pbs.outputFile, options(generateSvg), options(generatePng))
closeDotAndRunGraphviz(pbs)
}
}