github.com/emcfarlane/larking@v0.0.0-20220605172417-1704b45ee6c3/builder/graph.go (about)

     1  package builder
     2  
     3  import (
     4  	"bytes"
     5  	"fmt"
     6  	"io"
     7  	"net"
     8  	"net/http"
     9  	"os"
    10  	"os/exec"
    11  	"strings"
    12  
    13  	"github.com/emcfarlane/larking/starlib"
    14  	"github.com/emcfarlane/larking/starlib/starlarkrule"
    15  	"github.com/emcfarlane/larking/starlib/starlarkthread"
    16  	"go.starlark.net/starlark"
    17  )
    18  
    19  // TODO: build dot-graph
    20  // https://graphviz.org/Gallery/directed/bazel.html
    21  // https://graphviz.org/Gallery/directed/go-package.html
    22  
    23  // https://github.com/google/pprof/blob/83db2b799d1f74c40857232cb5eb4c60379fe6c2/internal/driver/webui.go#L332
    24  func dotToSvg(dot []byte) ([]byte, error) {
    25  	cmd := exec.Command("dot", "-Tsvg")
    26  	out := &bytes.Buffer{}
    27  	cmd.Stdin, cmd.Stdout, cmd.Stderr = bytes.NewBuffer(dot), out, os.Stderr
    28  	if err := cmd.Run(); err != nil {
    29  		return nil, err
    30  	}
    31  
    32  	// Fix dot bug related to unquoted ampersands.
    33  	svg := bytes.ReplaceAll(out.Bytes(), []byte("&;"), []byte("&;"))
    34  
    35  	// Cleanup for embedding by dropping stuff before the <svg> start.
    36  	if pos := bytes.Index(svg, []byte("<svg")); pos >= 0 {
    37  		svg = svg[pos:]
    38  	}
    39  	return svg, nil
    40  }
    41  
    42  func generateDot(a *starlarkrule.Action) ([]byte, error) {
    43  	var (
    44  		buf  bytes.Buffer
    45  		tabs = 0
    46  	)
    47  	p := func(ss ...string) {
    48  		for i := 0; i < tabs; i++ {
    49  			buf.WriteRune('\t')
    50  		}
    51  		for _, s := range ss {
    52  			buf.WriteString(s)
    53  		}
    54  		buf.WriteRune('\n')
    55  	}
    56  	q := `"`
    57  
    58  	p(`digraph laze {`)
    59  	tabs += 1
    60  	p(`fontname="Helvetica,Arial,sans-serif"`)
    61  	p(`node [fontname="Helvetica,Arial,sans-serif"]`)
    62  	p(`edge [fontname="Helvetica,Arial,sans-serif"]`)
    63  	p(`node [shape=box];`)
    64  	//p(`rankdir="LR"`)
    65  
    66  	deps := []*starlarkrule.Action{a}
    67  	for n := len(deps); n > 0; n = len(deps) {
    68  		a, deps = deps[n-1], deps[:n-1] // pop
    69  
    70  		p(q, a.Key(), q)
    71  
    72  		deps = append(deps, a.Deps...)
    73  		for _, at := range a.Deps {
    74  			p(q, at.Key(), q, " -> ", q, a.Key(), q)
    75  		}
    76  	}
    77  
    78  	tabs -= 1
    79  	p(`}`)
    80  
    81  	return buf.Bytes(), nil
    82  }
    83  
    84  var isLocalhost = map[string]bool{
    85  	"localhost": true,
    86  	"127.0.0.1": true,
    87  	"[::1]":     true,
    88  	"::1":       true,
    89  }
    90  
    91  func Serve(l net.Listener) error { //addr string) error {
    92  	host, _, err := net.SplitHostPort(l.Addr().String())
    93  	if err != nil {
    94  		return err
    95  	}
    96  
    97  	dir := ""
    98  	label, err := starlarkrule.ParseRelativeLabel("file://./?metadata=skip", dir)
    99  	if err != nil {
   100  		return err
   101  	}
   102  
   103  	b, err := starlarkrule.NewBuilder(label)
   104  	if err != nil {
   105  		return err
   106  	}
   107  
   108  	globals := starlib.NewGlobals()
   109  	loader := starlib.NewLoader(globals)
   110  
   111  	resources := starlarkthread.ResourceStore{} // resources
   112  	defer resources.Close()
   113  
   114  	isLocal := isLocalhost[host]
   115  	handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
   116  		if isLocal {
   117  			// Only allow local clients
   118  			host, _, err := net.SplitHostPort(r.RemoteAddr)
   119  			if err != nil || !isLocalhost[host] {
   120  				http.Error(w, "permission denied", http.StatusForbidden)
   121  				return
   122  			}
   123  		}
   124  
   125  		name := strings.TrimPrefix(r.URL.Path, "/graph/")
   126  		l, err := label.Parse(name)
   127  		if err != nil {
   128  			http.Error(w, err.Error(), http.StatusBadRequest)
   129  			return
   130  		}
   131  
   132  		ctx := r.Context()
   133  		thread := &starlark.Thread{
   134  			Name: l.String(),
   135  			Load: loader.Load,
   136  		}
   137  		starlarkthread.SetResourceStore(thread, &resources)
   138  		starlarkthread.SetContext(thread, ctx)
   139  
   140  		action, err := b.Build(thread, label)
   141  		if err != nil {
   142  			http.Error(w, err.Error(), http.StatusBadRequest)
   143  			return
   144  		}
   145  
   146  		dot, err := generateDot(action)
   147  		if err != nil {
   148  			http.Error(w, err.Error(), http.StatusInternalServerError)
   149  			return
   150  		}
   151  		fmt.Println("------ dot -------")
   152  		fmt.Println(string(dot))
   153  		fmt.Println("------ dot -------")
   154  
   155  		svg, err := dotToSvg(dot)
   156  		if err != nil {
   157  			http.Error(w, err.Error(), http.StatusInternalServerError)
   158  			return
   159  		}
   160  
   161  		var buf bytes.Buffer
   162  
   163  		buf.WriteString(`<html>
   164      <head>
   165          <title>Laze</title>
   166      </head>
   167      <body>`)
   168  		buf.Write(svg)
   169  		buf.WriteString(`    </body>
   170  </html>`)
   171  
   172  		w.Header().Set("Content-Type", "text/html")
   173  		if _, err := io.Copy(w, &buf); err != nil {
   174  			http.Error(w, err.Error(), http.StatusInternalServerError)
   175  			return
   176  		}
   177  
   178  	})
   179  
   180  	mux := http.NewServeMux()
   181  	//mux.Handle("/", handler)
   182  	mux.Handle("/graph/", handler)
   183  	s := &http.Server{Handler: mux}
   184  	return s.Serve(l)
   185  }