package drmdb
import (
"encoding/json"
"fmt"
"io"
"net/http"
"os"
"sort"
"strconv"
"strings"
"git.sr.ht/~emersion/drmdb/database"
"git.sr.ht/~emersion/drmdb/drmdoc"
"git.sr.ht/~emersion/drmdb/drmtree"
"git.sr.ht/~emersion/drmdb/treefmt"
"git.sr.ht/~emersion/go-drm"
"git.sr.ht/~emersion/go-hwids"
"github.com/labstack/echo/v4"
)
func badRequest(c echo.Context, msg string, err error) error {
if err != nil {
c.Logger().Printf("%v: %v", msg, err)
}
return c.String(http.StatusBadRequest, "Error: "+msg+"\n")
}
func loadPCIIDs() (vendors map[uint16]string, devices map[uint32]string, err error) {
vendors = make(map[uint16]string)
devices = make(map[uint32]string)
db, err := hwids.OpenPCI()
if err != nil {
return nil, nil, err
}
defer db.Close()
for {
id, err := db.NextID()
if err == io.EOF {
break
} else if err != nil {
return nil, nil, err
}
switch id.Type {
case hwids.PCIVendor:
vendors[id.Vendor] = id.VendorName
case hwids.PCIDevice:
k := uint32(id.Vendor)<<16 | uint32(id.Device)
devices[k] = id.DeviceName
}
}
return vendors, devices, db.Close()
}
func New() *echo.Echo {
e := echo.New()
db, err := database.Open()
if err != nil {
e.Logger.Fatal("Failed to open database:", err)
}
e.Renderer, err = loadTemplates()
if err != nil {
e.Logger.Fatal("Failed to load templates:", err)
}
pciVendors, pciDevices, err := loadPCIIDs()
if err != nil {
e.Logger.Error("Failed to load PCI IDs:", err)
}
e.GET("/", func(c echo.Context) error {
data := struct {
Host string
NumSnapshots int
NumDevices int
NumDrivers int
}{}
data.Host = c.Request().Host
devices := make(map[string]struct{})
drivers := make(map[string]struct{})
err := db.Walk(func(k string, n *drmtree.Node) error {
data.NumSnapshots++
devices[n.Device.BusID()] = struct{}{}
drivers[n.Driver.Name] = struct{}{}
return nil
})
if err != nil {
return err
}
data.NumDevices = len(devices)
data.NumDrivers = len(drivers)
return c.Render(http.StatusOK, "index.html", &data)
})
e.POST("/submit", func(c echo.Context) error {
lr := io.LimitedReader{R: c.Request().Body, N: 512 * 1024}
var nodes drmtree.NodeMap
if err := json.NewDecoder(&lr).Decode(&nodes); err != nil {
return badRequest(c, "invalid JSON", err)
} else if len(nodes) == 0 {
return badRequest(c, "data is empty", nil)
}
success := false
for name, n := range nodes {
if key, err := db.Store(n); err != nil {
fmt.Fprintf(c.Response(), "%s: error: %v\n", name, err)
} else {
u := "https://" + c.Request().Host + "/devices/" + key
fmt.Fprintf(c.Response(), "%s: data uploaded to %s\n", name, u)
success = true
}
}
if success {
fmt.Fprintf(c.Response(), "Thanks!\n")
}
return nil
})
e.GET("/snapshot.tar.gz", func(c echo.Context) error {
h := c.Response().Header()
h.Set("Content-Type", "application/gzip")
h.Set("Content-Disposition", "attachment; filename=\"drmdb-snapshot.tar.gz\"")
return writeSnapshot(c.Response())
})
e.GET("/drivers", func(c echo.Context) error {
var drivers []*drmtree.Driver
err := walkLatest(db, walkLatestDriver, func(k string, n *drmtree.Node) error {
drivers = append(drivers, n.Driver)
return nil
})
if err != nil {
return err
}
return c.Render(http.StatusOK, "drivers.html", struct {
Drivers []*drmtree.Driver
}{drivers})
})
e.GET("/devices", func(c echo.Context) error {
driverFilter := c.QueryParam("driver")
type deviceData struct {
Key string
BusID string
BusType drm.BusType
Vendor string
Name string
Driver string
}
var devices []deviceData
err := walkLatest(db, walkLatestDevice, func(k string, n *drmtree.Node) error {
if driverFilter != "" && n.Driver.Name != driverFilter {
return nil
}
data := deviceData{
Key: k,
BusID: n.Device.BusID(),
BusType: n.Device.BusType,
Driver: n.Driver.Name,
}
switch dev := n.Device.DeviceData.(type) {
case *drmtree.DevicePCI:
data.Vendor = pciVendors[uint16(dev.Vendor)]
data.Name = pciDevices[dev.Vendor<<16|dev.Device]
case *drmtree.DevicePlatform:
// No-op
default:
return nil
}
devices = append(devices, data)
return nil
})
if err != nil {
return err
}
return c.Render(http.StatusOK, "devices.html", struct {
Devices []deviceData
}{devices})
})
e.GET("/devices/:key", func(c echo.Context) error {
type altDeviceData struct {
Key string
Driver *drmtree.Driver
}
key := c.Param("key")
raw := false
if strings.HasSuffix(key, ".json") {
key = strings.TrimSuffix(key, ".json")
raw = true
}
n, err := db.Load(key)
if err != nil {
if os.IsNotExist(err) {
return c.String(http.StatusNotFound, "no such device")
}
return err
}
if raw {
return c.JSON(http.StatusOK, n)
} else {
var altDevices []altDeviceData
err := db.Walk(func(k string, alt *drmtree.Node) error {
if k == key {
return nil
}
if alt.Device.BusType != n.Device.BusType || alt.Device.BusID() != n.Device.BusID() {
return nil
}
altDevices = append(altDevices, altDeviceData{
Key: k,
Driver: alt.Driver,
})
return nil
})
if err != nil {
return err
}
sort.Slice(altDevices, func(i, j int) bool {
a, b := altDevices[i], altDevices[j]
return !driverLess(a.Driver, b.Driver)
})
tf := treefmt.NewMemoryFormatter()
n.FormatTree(tf)
return c.Render(http.StatusOK, "device.html", struct {
Key string
Node *drmtree.Node
Tree []treefmt.Memory
AltDevices []altDeviceData
}{key, n, tf.Tree(), altDevices})
}
})
e.GET("/capabilities", func(c echo.Context) error {
driverName := c.QueryParam("driver")
var drivers []string
caps := make(map[string]map[string]*uint64)
clientCaps := make(map[string]map[string]bool)
err := walkLatest(db, walkLatestDriver, func(k string, n *drmtree.Node) error {
drv := n.Driver.Name
if driverName != "" && drv != driverName {
return nil
}
drivers = append(drivers, drv)
for name, val := range n.Driver.Caps {
if _, ok := caps[name]; !ok {
caps[name] = make(map[string]*uint64)
}
caps[name][drv] = val
}
for name, supported := range n.Driver.ClientCaps {
if _, ok := clientCaps[name]; !ok {
clientCaps[name] = make(map[string]bool)
}
clientCaps[name][drv] = supported
}
return nil
})
if err != nil {
return err
}
return c.Render(http.StatusOK, "capabilities.html", struct {
Drivers []string
Caps map[string]map[string]*uint64
ClientCaps map[string]map[string]bool
}{drivers, caps, clientCaps})
})
e.GET("/properties", func(c echo.Context) error {
type propertyData struct {
Type drm.PropertyType
ObjectType drm.ObjectType
Drivers map[string]bool
}
var objectType drm.ObjectType
if s := c.QueryParam("object-type"); s != "" {
if i, err := strconv.Atoi(s); err != nil {
return c.String(http.StatusBadRequest, "invalid object type")
} else {
objectType = drm.ObjectType(i)
}
}
driverName := c.QueryParam("driver")
drivers := make(map[string]struct{})
props := make(map[string]propertyData)
err := db.Walk(func(k string, n *drmtree.Node) error {
drv := n.Driver.Name
if driverName != "" && drv != driverName {
return nil
}
drivers[drv] = struct{}{}
return walkNodeProps(n, func(obj drm.AnyID, name string, prop *drmtree.Property) error {
if objectType != drm.ObjectAny && obj.Type() != objectType {
return nil
}
if _, ok := props[name]; !ok {
props[name] = propertyData{
Type: prop.Type,
ObjectType: obj.Type(),
Drivers: make(map[string]bool),
}
}
props[name].Drivers[drv] = true
return nil
})
})
if err != nil {
return err
}
return c.Render(http.StatusOK, "properties.html", struct {
Drivers map[string]struct{}
Properties map[string]propertyData
}{drivers, props})
})
e.GET("/properties/:obj/:name", func(c echo.Context) error {
var objectType drm.ObjectType
if i, err := strconv.Atoi(c.Param("obj")); err != nil {
return c.String(http.StatusBadRequest, "invalid object type")
} else {
objectType = drm.ObjectType(i)
}
propertyName := c.Param("name")
doc := drmdoc.Prop(objectType, propertyName)
var property drmtree.Property
found := false
var spec interface{}
drivers := make(map[string]bool)
err := db.Walk(func(k string, n *drmtree.Node) error {
if _, ok := drivers[n.Driver.Name]; !ok {
drivers[n.Driver.Name] = false
}
return walkNodeProps(n, func(obj drm.AnyID, name string, p *drmtree.Property) error {
if obj.Type() != objectType || name != propertyName {
return nil
}
if found && property.Type != p.Type {
return fmt.Errorf("inconsistent property types: %v and %v", property.Type, p.Type)
}
drivers[n.Driver.Name] = true
property = *p
found = true
if spec == nil {
spec = p.Spec
} else if p.Spec != nil {
var err error
if spec, err = mergeSpec(spec, p.Spec); err != nil {
return fmt.Errorf("inconsistent property spec: %v", err)
}
}
return nil
})
})
if err != nil {
return err
}
if !found {
return c.String(http.StatusNotFound, "property not found")
}
var flags []string
if property.Atomic {
flags = append(flags, "atomic")
}
if property.Immutable {
flags = append(flags, "immutable")
}
return c.Render(http.StatusOK, "property.html", struct {
Name string
Prop *drmtree.Property
ObjectType drm.ObjectType
Flags []string
Spec interface{}
Drivers map[string]bool
Doc string
}{propertyName, &property, objectType, flags, spec, drivers, doc})
})
e.GET("/formats", func(c echo.Context) error {
type modifierAndFormat struct {
mod drm.Modifier
fmt drm.Format
}
type formatData struct {
Modifier drm.Modifier
Format drm.Format
Planes map[drm.PlaneType]int
Drivers map[string]int
}
planeFilter := -1
if s := c.QueryParam("plane"); s != "" {
var err error
if planeFilter, err = strconv.Atoi(s); err != nil {
return c.String(http.StatusBadRequest, "invalid plane")
}
}
driverFilter := c.QueryParam("driver")
// For each (modifier, format) pair, build two tables by walking each
// device:
// - Number of devices supporting it by plane type (max: number of
// devices having such a plane)
// - Number of devices supporting it by driver (max: number of devices
// supported by the driver)
planes := make(map[drm.PlaneType]int)
drivers := make(map[string]int)
formats := make(map[modifierAndFormat]formatData)
err := walkLatest(db, walkLatestDevice, func(k string, n *drmtree.Node) error {
if driverFilter != "" && n.Driver.Name != driverFilter {
return nil
}
drivers[n.Driver.Name]++
planeFormats := make(map[drm.PlaneType]map[modifierAndFormat]bool)
driverFormats := make(map[modifierAndFormat]bool)
for _, plane := range n.Planes {
if planeFilter >= 0 && plane.Type() != drm.PlaneType(planeFilter) {
continue
}
if _, ok := planeFormats[plane.Type()]; !ok {
planeFormats[plane.Type()] = make(map[modifierAndFormat]bool)
planes[plane.Type()]++
}
inFormats := plane.InFormats()
for _, mod := range inFormats {
for _, fmt := range mod.Formats {
k := modifierAndFormat{mod.Modifier, fmt}
if _, ok := formats[k]; !ok {
formats[k] = formatData{
Modifier: mod.Modifier,
Format: fmt,
Planes: make(map[drm.PlaneType]int),
Drivers: make(map[string]int),
}
}
if !planeFormats[plane.Type()][k] {
formats[k].Planes[plane.Type()]++
planeFormats[plane.Type()][k] = true
}
if !driverFormats[k] {
formats[k].Drivers[n.Driver.Name]++
driverFormats[k] = true
}
}
}
}
return nil
})
if err != nil {
return err
}
return c.Render(http.StatusOK, "formats.html", struct {
Planes map[drm.PlaneType]int
Drivers map[string]int
Formats map[modifierAndFormat]formatData
}{planes, drivers, formats})
})
e.Static("/assets", "public/assets")
e.Use(func(next echo.HandlerFunc) echo.HandlerFunc {
return func(ctx echo.Context) error {
if ctx.Request().Method == http.MethodGet {
ctx.Response().Header().Set("Cache-Control", "public, max-age=3600")
}
return next(ctx)
}
})
return e
}