github.com/google/syzkaller@v0.0.0-20251211124644-a066d2bc4b02/tools/syz-testbed/html.go (about)

     1  // Copyright 2021 syzkaller project authors. All rights reserved.
     2  // Use of this source code is governed by Apache 2 LICENSE that can be found in the LICENSE file.
     3  
     4  package main
     5  
     6  import (
     7  	"bytes"
     8  	"embed"
     9  	"fmt"
    10  	"html/template"
    11  	"log"
    12  	"net"
    13  	"net/http"
    14  	"net/url"
    15  	"os"
    16  	"time"
    17  
    18  	"github.com/google/syzkaller/pkg/html/pages"
    19  	"github.com/google/syzkaller/pkg/osutil"
    20  	"github.com/gorilla/handlers"
    21  )
    22  
    23  func (ctx *TestbedContext) setupHTTPServer() {
    24  	mux := http.NewServeMux()
    25  
    26  	mux.HandleFunc("/", ctx.httpMain)
    27  	mux.HandleFunc("/graph", ctx.httpGraph)
    28  	mux.HandleFunc("/favicon.ico", ctx.httpFavicon)
    29  
    30  	listener, err := net.Listen("tcp", ctx.Config.HTTP)
    31  	if err != nil {
    32  		log.Fatalf("failed to listen on %s", ctx.Config.HTTP)
    33  	}
    34  
    35  	log.Printf("handling HTTP on %s", listener.Addr())
    36  	go func() {
    37  		err := http.Serve(listener, handlers.CompressHandler(mux))
    38  		if err != nil {
    39  			log.Fatalf("failed to listen on %v: %v", ctx.Config.HTTP, err)
    40  		}
    41  	}()
    42  }
    43  
    44  func (ctx *TestbedContext) httpFavicon(w http.ResponseWriter, r *http.Request) {
    45  	http.Error(w, "Not Found", http.StatusNotFound)
    46  }
    47  
    48  func (ctx *TestbedContext) getCurrentStatView(r *http.Request) (*StatView, error) {
    49  	views, err := ctx.GetStatViews()
    50  	if err != nil {
    51  		return nil, err
    52  	}
    53  	if len(views) == 0 {
    54  		return nil, fmt.Errorf("no stat views available")
    55  	}
    56  	viewName := r.FormValue("view")
    57  	if viewName != "" {
    58  		var targetView *StatView
    59  		for _, view := range views {
    60  			if view.Name == viewName {
    61  				targetView = &view
    62  				break
    63  			}
    64  		}
    65  		if targetView == nil {
    66  			return nil, fmt.Errorf("the requested view is not found")
    67  		}
    68  		return targetView, nil
    69  	}
    70  	// No specific view is requested.
    71  	// First try to find the first non-empty one.
    72  	for _, view := range views {
    73  		if !view.IsEmpty() {
    74  			return &view, nil
    75  		}
    76  	}
    77  	return &views[0], nil
    78  }
    79  
    80  func (ctx *TestbedContext) httpGraph(w http.ResponseWriter, r *http.Request) {
    81  	over := r.FormValue("over")
    82  
    83  	if ctx.Config.BenchCmp == "" {
    84  		http.Error(w, "the path to the benchcmp tool is not specified", http.StatusInternalServerError)
    85  		return
    86  	}
    87  
    88  	targetView, err := ctx.getCurrentStatView(r)
    89  	if err != nil {
    90  		http.Error(w, fmt.Sprintf("%s", err), http.StatusInternalServerError)
    91  		return
    92  	}
    93  
    94  	// TODO: move syz-benchcmp functionality to pkg/ and just import it?
    95  	dir, err := os.MkdirTemp("", "")
    96  	if err != nil {
    97  		http.Error(w, "failed to create temp folder", http.StatusInternalServerError)
    98  		return
    99  	}
   100  	defer os.RemoveAll(dir)
   101  
   102  	file, err := osutil.TempFile("")
   103  	if err != nil {
   104  		http.Error(w, "failed to create temp file", http.StatusInternalServerError)
   105  		return
   106  	}
   107  	defer os.Remove(file)
   108  
   109  	benches, err := targetView.SaveAvgBenches(dir)
   110  	if err != nil {
   111  		http.Error(w, "failed to save avg benches", http.StatusInternalServerError)
   112  		return
   113  	}
   114  
   115  	args := append([]string{"-all", "-over", over, "-out", file}, benches...)
   116  	if out, err := osutil.RunCmd(time.Hour, "", ctx.Config.BenchCmp, args...); err != nil {
   117  		http.Error(w, "syz-benchcmp failed\n"+string(out), http.StatusInternalServerError)
   118  		return
   119  	}
   120  
   121  	data, err := os.ReadFile(file)
   122  	if err != nil {
   123  		http.Error(w, "failed to read the temporary file", http.StatusInternalServerError)
   124  		return
   125  	}
   126  	w.Write(data)
   127  }
   128  
   129  type uiTable struct {
   130  	Table     *Table
   131  	ColumnURL func(string) string
   132  	RowURL    func(string) string
   133  	Extra     bool
   134  	HasFooter bool
   135  	AlignedBy string
   136  }
   137  
   138  const (
   139  	HTMLStatsTable         = "stats"
   140  	HTMLBugsTable          = "bugs"
   141  	HTMLBugCountsTable     = "bug_counts"
   142  	HTMLReprosTable        = "repros"
   143  	HTMLCReprosTable       = "crepros"
   144  	HTMLReproAttemptsTable = "repro_attempts"
   145  	HTMLReproDurationTable = "repro_duration"
   146  )
   147  
   148  type uiTableGenerator = func(urlPrefix string, view *StatView, r *http.Request) (*uiTable, error)
   149  
   150  type uiTableType struct {
   151  	Key       string
   152  	Title     string
   153  	Generator uiTableGenerator
   154  }
   155  
   156  type uiStatView struct {
   157  	Name            string
   158  	TableTypes      map[string]uiTableType
   159  	ActiveTableType string
   160  	ActiveTable     *uiTable
   161  	GenTableURL     func(uiTableType) string
   162  }
   163  
   164  type uiMainPage struct {
   165  	Name       string
   166  	Summary    uiTable
   167  	Views      []StatView
   168  	ActiveView uiStatView
   169  }
   170  
   171  func (ctx *TestbedContext) getTableTypes() []uiTableType {
   172  	allTypeList := []uiTableType{
   173  		{HTMLStatsTable, "Statistics", ctx.httpMainStatsTable},
   174  		{HTMLBugsTable, "Bugs", ctx.genSimpleTableController((*StatView).GenerateBugTable, true)},
   175  		{HTMLBugCountsTable, "Bug Counts", ctx.genSimpleTableController((*StatView).GenerateBugCountsTable, false)},
   176  		{HTMLReprosTable, "Repros", ctx.genSimpleTableController((*StatView).GenerateReproSuccessTable, true)},
   177  		{HTMLCReprosTable, "C Repros", ctx.genSimpleTableController((*StatView).GenerateCReproSuccessTable, true)},
   178  		{HTMLReproAttemptsTable, "All Repros", ctx.genSimpleTableController((*StatView).GenerateReproAttemptsTable, false)},
   179  		{HTMLReproDurationTable, "Duration", ctx.genSimpleTableController((*StatView).GenerateReproDurationTable, true)},
   180  	}
   181  	typeList := []uiTableType{}
   182  	for _, t := range allTypeList {
   183  		if ctx.Target.SupportsHTMLView(t.Key) {
   184  			typeList = append(typeList, t)
   185  		}
   186  	}
   187  	return typeList
   188  }
   189  
   190  func (ctx *TestbedContext) genSimpleTableController(method func(view *StatView) (*Table, error),
   191  	hasFooter bool) uiTableGenerator {
   192  	return func(urlPrefix string, view *StatView, r *http.Request) (*uiTable, error) {
   193  		table, err := method(view)
   194  		if err != nil {
   195  			return nil, fmt.Errorf("table generation failed: %w", err)
   196  		}
   197  		return &uiTable{
   198  			Table:     table,
   199  			HasFooter: hasFooter,
   200  		}, nil
   201  	}
   202  }
   203  
   204  func (ctx *TestbedContext) httpMainStatsTable(urlPrefix string, view *StatView, r *http.Request) (*uiTable, error) {
   205  	alignBy := r.FormValue("align")
   206  	table, err := view.AlignedStatsTable(alignBy)
   207  	if err != nil {
   208  		return nil, fmt.Errorf("stat table generation failed: %w", err)
   209  	}
   210  	baseColumn := r.FormValue("base_column")
   211  	if baseColumn != "" {
   212  		err := table.SetRelativeValues(baseColumn)
   213  		if err != nil {
   214  			log.Printf("failed to execute SetRelativeValues: %s", err)
   215  		}
   216  	}
   217  
   218  	return &uiTable{
   219  		Table: table,
   220  		Extra: baseColumn != "",
   221  		ColumnURL: func(column string) string {
   222  			if column == baseColumn {
   223  				return ""
   224  			}
   225  			v := url.Values{}
   226  			v.Set("base_column", column)
   227  			v.Set("align", alignBy)
   228  			return urlPrefix + v.Encode()
   229  		},
   230  		RowURL: func(row string) string {
   231  			if row == alignBy {
   232  				return ""
   233  			}
   234  			v := url.Values{}
   235  			v.Set("base_column", baseColumn)
   236  			v.Set("align", row)
   237  			return urlPrefix + v.Encode()
   238  		},
   239  		AlignedBy: alignBy,
   240  	}, nil
   241  }
   242  
   243  func (ctx *TestbedContext) httpMain(w http.ResponseWriter, r *http.Request) {
   244  	activeView, err := ctx.getCurrentStatView(r)
   245  	if err != nil {
   246  		http.Error(w, fmt.Sprintf("%s", err), http.StatusInternalServerError)
   247  		return
   248  	}
   249  	views, err := ctx.GetStatViews()
   250  	if err != nil {
   251  		http.Error(w, fmt.Sprintf("%s", err), http.StatusInternalServerError)
   252  		return
   253  	}
   254  	uiView := uiStatView{Name: activeView.Name}
   255  	tableTypes := ctx.getTableTypes()
   256  	if len(tableTypes) == 0 {
   257  		http.Error(w, "No tables are available", http.StatusInternalServerError)
   258  		return
   259  	}
   260  	uiView.TableTypes = map[string]uiTableType{}
   261  	for _, table := range tableTypes {
   262  		uiView.TableTypes[table.Key] = table
   263  	}
   264  	uiView.ActiveTableType = r.FormValue("table")
   265  	if uiView.ActiveTableType == "" {
   266  		uiView.ActiveTableType = tableTypes[0].Key
   267  	}
   268  	tableType, found := uiView.TableTypes[uiView.ActiveTableType]
   269  	if !found {
   270  		http.Error(w, fmt.Sprintf("%s", err), http.StatusInternalServerError)
   271  		return
   272  	}
   273  	uiView.GenTableURL = func(t uiTableType) string {
   274  		v := url.Values{}
   275  		v.Set("view", activeView.Name)
   276  		v.Set("table", t.Key)
   277  		return "/?" + v.Encode()
   278  	}
   279  	uiView.ActiveTable, err = tableType.Generator(uiView.GenTableURL(tableType)+"&", activeView, r)
   280  	if err != nil {
   281  		http.Error(w, fmt.Sprintf("%s", err), http.StatusInternalServerError)
   282  		return
   283  	}
   284  	data := &uiMainPage{
   285  		Name:       ctx.Config.Name,
   286  		Summary:    uiTable{Table: ctx.TestbedStatsTable()},
   287  		Views:      views,
   288  		ActiveView: uiView,
   289  	}
   290  
   291  	executeTemplate(w, mainTemplate, "testbed.html", data)
   292  }
   293  
   294  func executeTemplate(w http.ResponseWriter, templ *template.Template, name string, data interface{}) {
   295  	buf := new(bytes.Buffer)
   296  	if err := templ.ExecuteTemplate(buf, name, data); err != nil {
   297  		log.Printf("failed to execute template: %v", err)
   298  		http.Error(w, fmt.Sprintf("failed to execute template: %v", err), http.StatusInternalServerError)
   299  		return
   300  	}
   301  	w.Write(buf.Bytes())
   302  }
   303  
   304  //go:embed templates
   305  var testbedTemplates embed.FS
   306  var mainTemplate = pages.CreateFromFS(testbedTemplates, "templates/*.html")