internal/lsp: debug pages for sessions views and files
Change-Id: Id21f391bd66513615d274588ce7d1d1efe407074 Reviewed-on: https://go-review.googlesource.com/c/tools/+/179438 Run-TryBot: Ian Cottrell <iancottrell@google.com> Reviewed-by: Rebecca Stambler <rstambler@golang.org>
This commit is contained in:
parent
08e0b306e8
commit
df5f646307
|
@ -8,31 +8,43 @@ import (
|
||||||
"crypto/sha1"
|
"crypto/sha1"
|
||||||
"fmt"
|
"fmt"
|
||||||
"go/token"
|
"go/token"
|
||||||
|
"strconv"
|
||||||
|
"sync/atomic"
|
||||||
|
|
||||||
|
"golang.org/x/tools/internal/lsp/debug"
|
||||||
"golang.org/x/tools/internal/lsp/source"
|
"golang.org/x/tools/internal/lsp/source"
|
||||||
"golang.org/x/tools/internal/lsp/xlog"
|
"golang.org/x/tools/internal/lsp/xlog"
|
||||||
"golang.org/x/tools/internal/span"
|
"golang.org/x/tools/internal/span"
|
||||||
)
|
)
|
||||||
|
|
||||||
func New() source.Cache {
|
func New() source.Cache {
|
||||||
return &cache{
|
index := atomic.AddInt64(&cacheIndex, 1)
|
||||||
|
c := &cache{
|
||||||
|
id: strconv.FormatInt(index, 10),
|
||||||
fset: token.NewFileSet(),
|
fset: token.NewFileSet(),
|
||||||
}
|
}
|
||||||
|
debug.AddCache(debugCache{c})
|
||||||
|
return c
|
||||||
}
|
}
|
||||||
|
|
||||||
type cache struct {
|
type cache struct {
|
||||||
nativeFileSystem
|
nativeFileSystem
|
||||||
|
|
||||||
|
id string
|
||||||
fset *token.FileSet
|
fset *token.FileSet
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *cache) NewSession(log xlog.Logger) source.Session {
|
func (c *cache) NewSession(log xlog.Logger) source.Session {
|
||||||
return &session{
|
index := atomic.AddInt64(&sessionIndex, 1)
|
||||||
|
s := &session{
|
||||||
cache: c,
|
cache: c,
|
||||||
|
id: strconv.FormatInt(index, 10),
|
||||||
log: log,
|
log: log,
|
||||||
overlays: make(map[span.URI]*source.FileContent),
|
overlays: make(map[span.URI]*source.FileContent),
|
||||||
filesWatchMap: NewWatchMap(),
|
filesWatchMap: NewWatchMap(),
|
||||||
}
|
}
|
||||||
|
debug.AddSession(debugSession{s})
|
||||||
|
return s
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *cache) FileSet() *token.FileSet {
|
func (c *cache) FileSet() *token.FileSet {
|
||||||
|
@ -44,3 +56,10 @@ func hashContents(contents []byte) string {
|
||||||
// This hash is used for internal identity detection only
|
// This hash is used for internal identity detection only
|
||||||
return fmt.Sprintf("%x", sha1.Sum(contents))
|
return fmt.Sprintf("%x", sha1.Sum(contents))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var cacheIndex, sessionIndex, viewIndex int64
|
||||||
|
|
||||||
|
type debugCache struct{ *cache }
|
||||||
|
|
||||||
|
func (c *cache) ID() string { return c.id }
|
||||||
|
func (c debugCache) FileSet() *token.FileSet { return c.fset }
|
||||||
|
|
|
@ -8,9 +8,13 @@ import (
|
||||||
"context"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
|
"sort"
|
||||||
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
|
"sync/atomic"
|
||||||
|
|
||||||
|
"golang.org/x/tools/internal/lsp/debug"
|
||||||
"golang.org/x/tools/internal/lsp/source"
|
"golang.org/x/tools/internal/lsp/source"
|
||||||
"golang.org/x/tools/internal/lsp/xlog"
|
"golang.org/x/tools/internal/lsp/xlog"
|
||||||
"golang.org/x/tools/internal/span"
|
"golang.org/x/tools/internal/span"
|
||||||
|
@ -18,6 +22,7 @@ import (
|
||||||
|
|
||||||
type session struct {
|
type session struct {
|
||||||
cache *cache
|
cache *cache
|
||||||
|
id string
|
||||||
// the logger to use to communicate back with the client
|
// the logger to use to communicate back with the client
|
||||||
log xlog.Logger
|
log xlog.Logger
|
||||||
|
|
||||||
|
@ -40,6 +45,7 @@ func (s *session) Shutdown(ctx context.Context) {
|
||||||
}
|
}
|
||||||
s.views = nil
|
s.views = nil
|
||||||
s.viewMap = nil
|
s.viewMap = nil
|
||||||
|
debug.DropSession(debugSession{s})
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *session) Cache() source.Cache {
|
func (s *session) Cache() source.Cache {
|
||||||
|
@ -47,12 +53,14 @@ func (s *session) Cache() source.Cache {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *session) NewView(name string, folder span.URI) source.View {
|
func (s *session) NewView(name string, folder span.URI) source.View {
|
||||||
|
index := atomic.AddInt64(&viewIndex, 1)
|
||||||
s.viewMu.Lock()
|
s.viewMu.Lock()
|
||||||
defer s.viewMu.Unlock()
|
defer s.viewMu.Unlock()
|
||||||
ctx := context.Background()
|
ctx := context.Background()
|
||||||
backgroundCtx, cancel := context.WithCancel(ctx)
|
backgroundCtx, cancel := context.WithCancel(ctx)
|
||||||
v := &view{
|
v := &view{
|
||||||
session: s,
|
session: s,
|
||||||
|
id: strconv.FormatInt(index, 10),
|
||||||
baseCtx: ctx,
|
baseCtx: ctx,
|
||||||
backgroundCtx: backgroundCtx,
|
backgroundCtx: backgroundCtx,
|
||||||
cancel: cancel,
|
cancel: cancel,
|
||||||
|
@ -73,6 +81,7 @@ func (s *session) NewView(name string, folder span.URI) source.View {
|
||||||
s.views = append(s.views, v)
|
s.views = append(s.views, v)
|
||||||
// we always need to drop the view map
|
// we always need to drop the view map
|
||||||
s.viewMap = make(map[span.URI]source.View)
|
s.viewMap = make(map[span.URI]source.View)
|
||||||
|
debug.AddView(debugView{v})
|
||||||
return v
|
return v
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -225,3 +234,58 @@ func (s *session) buildOverlay() map[string][]byte {
|
||||||
}
|
}
|
||||||
return overlays
|
return overlays
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type debugSession struct{ *session }
|
||||||
|
|
||||||
|
func (s debugSession) ID() string { return s.id }
|
||||||
|
func (s debugSession) Cache() debug.Cache { return debugCache{s.cache} }
|
||||||
|
func (s debugSession) Files() []*debug.File {
|
||||||
|
var files []*debug.File
|
||||||
|
seen := make(map[span.URI]*debug.File)
|
||||||
|
s.openFiles.Range(func(key interface{}, value interface{}) bool {
|
||||||
|
uri, ok := key.(span.URI)
|
||||||
|
if ok {
|
||||||
|
f := &debug.File{Session: s, URI: uri}
|
||||||
|
seen[uri] = f
|
||||||
|
files = append(files, f)
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
})
|
||||||
|
s.overlayMu.Lock()
|
||||||
|
defer s.overlayMu.Unlock()
|
||||||
|
for _, overlay := range s.overlays {
|
||||||
|
f, ok := seen[overlay.URI]
|
||||||
|
if !ok {
|
||||||
|
f = &debug.File{Session: s, URI: overlay.URI}
|
||||||
|
seen[overlay.URI] = f
|
||||||
|
files = append(files, f)
|
||||||
|
}
|
||||||
|
f.Data = string(overlay.Data)
|
||||||
|
f.Error = overlay.Error
|
||||||
|
f.Hash = overlay.Hash
|
||||||
|
}
|
||||||
|
sort.Slice(files, func(i int, j int) bool {
|
||||||
|
return files[i].URI < files[j].URI
|
||||||
|
})
|
||||||
|
return files
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s debugSession) File(hash string) *debug.File {
|
||||||
|
s.overlayMu.Lock()
|
||||||
|
defer s.overlayMu.Unlock()
|
||||||
|
for _, overlay := range s.overlays {
|
||||||
|
if overlay.Hash == hash {
|
||||||
|
return &debug.File{
|
||||||
|
Session: s,
|
||||||
|
URI: overlay.URI,
|
||||||
|
Data: string(overlay.Data),
|
||||||
|
Error: overlay.Error,
|
||||||
|
Hash: overlay.Hash,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return &debug.File{
|
||||||
|
Session: s,
|
||||||
|
Hash: hash,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -15,12 +15,14 @@ import (
|
||||||
"sync"
|
"sync"
|
||||||
|
|
||||||
"golang.org/x/tools/go/packages"
|
"golang.org/x/tools/go/packages"
|
||||||
|
"golang.org/x/tools/internal/lsp/debug"
|
||||||
"golang.org/x/tools/internal/lsp/source"
|
"golang.org/x/tools/internal/lsp/source"
|
||||||
"golang.org/x/tools/internal/span"
|
"golang.org/x/tools/internal/span"
|
||||||
)
|
)
|
||||||
|
|
||||||
type view struct {
|
type view struct {
|
||||||
session *session
|
session *session
|
||||||
|
id string
|
||||||
|
|
||||||
// mu protects all mutable state of the view.
|
// mu protects all mutable state of the view.
|
||||||
mu sync.Mutex
|
mu sync.Mutex
|
||||||
|
@ -157,6 +159,7 @@ func (v *view) shutdown(context.Context) {
|
||||||
v.cancel()
|
v.cancel()
|
||||||
v.cancel = nil
|
v.cancel = nil
|
||||||
}
|
}
|
||||||
|
debug.DropView(debugView{v})
|
||||||
}
|
}
|
||||||
|
|
||||||
// Ignore checks if the given URI is a URI we ignore.
|
// Ignore checks if the given URI is a URI we ignore.
|
||||||
|
@ -395,3 +398,8 @@ func (v *view) mapFile(uri span.URI, f viewFile) {
|
||||||
v.filesByBase[basename] = append(v.filesByBase[basename], f)
|
v.filesByBase[basename] = append(v.filesByBase[basename], f)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type debugView struct{ *view }
|
||||||
|
|
||||||
|
func (v debugView) ID() string { return v.id }
|
||||||
|
func (v debugView) Session() debug.Session { return debugSession{v.session} }
|
||||||
|
|
|
@ -7,29 +7,204 @@ package debug
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
"context"
|
"context"
|
||||||
|
"go/token"
|
||||||
"html/template"
|
"html/template"
|
||||||
"log"
|
"log"
|
||||||
"net"
|
"net"
|
||||||
"net/http"
|
"net/http"
|
||||||
_ "net/http/pprof" // pull in the standard pprof handlers
|
_ "net/http/pprof" // pull in the standard pprof handlers
|
||||||
|
"path"
|
||||||
|
"sync"
|
||||||
|
|
||||||
|
"golang.org/x/tools/internal/span"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Cache interface {
|
||||||
|
ID() string
|
||||||
|
FileSet() *token.FileSet
|
||||||
|
}
|
||||||
|
|
||||||
|
type Session interface {
|
||||||
|
ID() string
|
||||||
|
Cache() Cache
|
||||||
|
Files() []*File
|
||||||
|
File(hash string) *File
|
||||||
|
}
|
||||||
|
|
||||||
|
type View interface {
|
||||||
|
ID() string
|
||||||
|
Name() string
|
||||||
|
Folder() span.URI
|
||||||
|
Session() Session
|
||||||
|
}
|
||||||
|
|
||||||
|
type File struct {
|
||||||
|
Session Session
|
||||||
|
URI span.URI
|
||||||
|
Data string
|
||||||
|
Error error
|
||||||
|
Hash string
|
||||||
|
}
|
||||||
|
|
||||||
|
var (
|
||||||
|
mu sync.Mutex
|
||||||
|
data = struct {
|
||||||
|
Caches []Cache
|
||||||
|
Sessions []Session
|
||||||
|
Views []View
|
||||||
|
}{}
|
||||||
)
|
)
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
http.HandleFunc("/", Render(mainTmpl, nil))
|
http.HandleFunc("/", Render(mainTmpl, func(*http.Request) interface{} { return data }))
|
||||||
http.HandleFunc("/debug/", Render(debugTmpl, nil))
|
http.HandleFunc("/debug/", Render(debugTmpl, nil))
|
||||||
|
http.HandleFunc("/cache/", Render(cacheTmpl, getCache))
|
||||||
|
http.HandleFunc("/session/", Render(sessionTmpl, getSession))
|
||||||
|
http.HandleFunc("/view/", Render(viewTmpl, getView))
|
||||||
|
http.HandleFunc("/file/", Render(fileTmpl, getFile))
|
||||||
http.HandleFunc("/info", Render(infoTmpl, getInfo))
|
http.HandleFunc("/info", Render(infoTmpl, getInfo))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// AddCache adds a cache to the set being served
|
||||||
|
func AddCache(cache Cache) {
|
||||||
|
mu.Lock()
|
||||||
|
defer mu.Unlock()
|
||||||
|
data.Caches = append(data.Caches, cache)
|
||||||
|
}
|
||||||
|
|
||||||
|
// DropCache drops a cache from the set being served
|
||||||
|
func DropCache(cache Cache) {
|
||||||
|
mu.Lock()
|
||||||
|
defer mu.Unlock()
|
||||||
|
//find and remove the cache
|
||||||
|
if i, _ := findCache(cache.ID()); i >= 0 {
|
||||||
|
copy(data.Caches[i:], data.Caches[i+1:])
|
||||||
|
data.Caches[len(data.Caches)-1] = nil
|
||||||
|
data.Caches = data.Caches[:len(data.Caches)-1]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func findCache(id string) (int, Cache) {
|
||||||
|
for i, c := range data.Caches {
|
||||||
|
if c.ID() == id {
|
||||||
|
return i, c
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return -1, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func getCache(r *http.Request) interface{} {
|
||||||
|
mu.Lock()
|
||||||
|
defer mu.Unlock()
|
||||||
|
id := path.Base(r.URL.Path)
|
||||||
|
result := struct {
|
||||||
|
Cache
|
||||||
|
Sessions []Session
|
||||||
|
}{}
|
||||||
|
_, result.Cache = findCache(id)
|
||||||
|
|
||||||
|
// now find all the views that belong to this session
|
||||||
|
for _, v := range data.Sessions {
|
||||||
|
if v.Cache().ID() == id {
|
||||||
|
result.Sessions = append(result.Sessions, v)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
func findSession(id string) Session {
|
||||||
|
for _, c := range data.Sessions {
|
||||||
|
if c.ID() == id {
|
||||||
|
return c
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func getSession(r *http.Request) interface{} {
|
||||||
|
mu.Lock()
|
||||||
|
defer mu.Unlock()
|
||||||
|
id := path.Base(r.URL.Path)
|
||||||
|
result := struct {
|
||||||
|
Session
|
||||||
|
Views []View
|
||||||
|
}{
|
||||||
|
Session: findSession(id),
|
||||||
|
}
|
||||||
|
// now find all the views that belong to this session
|
||||||
|
for _, v := range data.Views {
|
||||||
|
if v.Session().ID() == id {
|
||||||
|
result.Views = append(result.Views, v)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
func findView(id string) View {
|
||||||
|
for _, c := range data.Views {
|
||||||
|
if c.ID() == id {
|
||||||
|
return c
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func getView(r *http.Request) interface{} {
|
||||||
|
mu.Lock()
|
||||||
|
defer mu.Unlock()
|
||||||
|
id := path.Base(r.URL.Path)
|
||||||
|
return findView(id)
|
||||||
|
}
|
||||||
|
|
||||||
|
func getFile(r *http.Request) interface{} {
|
||||||
|
mu.Lock()
|
||||||
|
defer mu.Unlock()
|
||||||
|
hash := path.Base(r.URL.Path)
|
||||||
|
sid := path.Base(path.Dir(r.URL.Path))
|
||||||
|
session := findSession(sid)
|
||||||
|
return session.File(hash)
|
||||||
|
}
|
||||||
|
|
||||||
func getInfo(r *http.Request) interface{} {
|
func getInfo(r *http.Request) interface{} {
|
||||||
buf := &bytes.Buffer{}
|
buf := &bytes.Buffer{}
|
||||||
PrintVersionInfo(buf, true, HTML)
|
PrintVersionInfo(buf, true, HTML)
|
||||||
return template.HTML(buf.String())
|
return template.HTML(buf.String())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// AddSession adds a session to the set being served
|
||||||
|
func AddSession(session Session) {
|
||||||
|
mu.Lock()
|
||||||
|
defer mu.Unlock()
|
||||||
|
data.Sessions = append(data.Sessions, session)
|
||||||
|
}
|
||||||
|
|
||||||
|
// DropSession drops a session from the set being served
|
||||||
|
func DropSession(session Session) {
|
||||||
|
mu.Lock()
|
||||||
|
defer mu.Unlock()
|
||||||
|
//find and remove the session
|
||||||
|
}
|
||||||
|
|
||||||
|
// AddView adds a view to the set being served
|
||||||
|
func AddView(view View) {
|
||||||
|
mu.Lock()
|
||||||
|
defer mu.Unlock()
|
||||||
|
data.Views = append(data.Views, view)
|
||||||
|
}
|
||||||
|
|
||||||
|
// DropView drops a view from the set being served
|
||||||
|
func DropView(view View) {
|
||||||
|
mu.Lock()
|
||||||
|
defer mu.Unlock()
|
||||||
|
//find and remove the view
|
||||||
|
}
|
||||||
|
|
||||||
// Serve starts and runs a debug server in the background.
|
// Serve starts and runs a debug server in the background.
|
||||||
// It also logs the port the server starts on, to allow for :0 auto assigned
|
// It also logs the port the server starts on, to allow for :0 auto assigned
|
||||||
// ports.
|
// ports.
|
||||||
func Serve(ctx context.Context, addr string) error {
|
func Serve(ctx context.Context, addr string) error {
|
||||||
|
mu.Lock()
|
||||||
|
defer mu.Unlock()
|
||||||
if addr == "" {
|
if addr == "" {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
@ -63,7 +238,7 @@ func Render(tmpl *template.Template, fun func(*http.Request) interface{}) func(h
|
||||||
var BaseTemplate = template.Must(template.New("").Parse(`
|
var BaseTemplate = template.Must(template.New("").Parse(`
|
||||||
<html>
|
<html>
|
||||||
<head>
|
<head>
|
||||||
<title>{{template "title"}}</title>
|
<title>{{template "title" .}}</title>
|
||||||
<style>
|
<style>
|
||||||
.profile-name{
|
.profile-name{
|
||||||
display:inline-block;
|
display:inline-block;
|
||||||
|
@ -82,11 +257,22 @@ Unknown page
|
||||||
{{end}}
|
{{end}}
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|
||||||
|
{{define "cachelink"}}<a href="/cache/{{.}}">Cache {{.}}</a>{{end}}
|
||||||
|
{{define "sessionlink"}}<a href="/session/{{.}}">Session {{.}}</a>{{end}}
|
||||||
|
{{define "viewlink"}}<a href="/view/{{.}}">View {{.}}</a>{{end}}
|
||||||
|
{{define "filelink"}}<a href="/file/{{.Session.ID}}/{{.Hash}}">{{.URI}}</a>{{end}}
|
||||||
`))
|
`))
|
||||||
|
|
||||||
var mainTmpl = template.Must(template.Must(BaseTemplate.Clone()).Parse(`
|
var mainTmpl = template.Must(template.Must(BaseTemplate.Clone()).Parse(`
|
||||||
{{define "title"}}GoPls server information{{end}}
|
{{define "title"}}GoPls server information{{end}}
|
||||||
{{define "body"}}
|
{{define "body"}}
|
||||||
|
<h2>Caches</h2>
|
||||||
|
<ul>{{range .Caches}}<li>{{template "cachelink" .ID}}</li>{{end}}</ul>
|
||||||
|
<h2>Sessions</h2>
|
||||||
|
<ul>{{range .Sessions}}<li>{{template "sessionlink" .ID}} from {{template "cachelink" .Cache.ID}}</li>{{end}}</ul>
|
||||||
|
<h2>Views</h2>
|
||||||
|
<ul>{{range .Views}}<li>{{.Name}} is {{template "viewlink" .ID}} from {{template "sessionlink" .Session.ID}} in {{.Folder}}</li>{{end}}</ul>
|
||||||
{{end}}
|
{{end}}
|
||||||
`))
|
`))
|
||||||
|
|
||||||
|
@ -100,6 +286,48 @@ var infoTmpl = template.Must(template.Must(BaseTemplate.Clone()).Parse(`
|
||||||
var debugTmpl = template.Must(template.Must(BaseTemplate.Clone()).Parse(`
|
var debugTmpl = template.Must(template.Must(BaseTemplate.Clone()).Parse(`
|
||||||
{{define "title"}}GoPls Debug pages{{end}}
|
{{define "title"}}GoPls Debug pages{{end}}
|
||||||
{{define "body"}}
|
{{define "body"}}
|
||||||
<A href="/debug/pprof">Profiling</A>
|
<a href="/debug/pprof">Profiling</a>
|
||||||
|
{{end}}
|
||||||
|
`))
|
||||||
|
|
||||||
|
var cacheTmpl = template.Must(template.Must(BaseTemplate.Clone()).Parse(`
|
||||||
|
{{define "title"}}Cache {{.ID}}{{end}}
|
||||||
|
{{define "body"}}
|
||||||
|
<h2>Sessions</h2>
|
||||||
|
<ul>{{range .Sessions}}<li>{{template "sessionlink" .ID}}</li>{{end}}</ul>
|
||||||
|
{{end}}
|
||||||
|
`))
|
||||||
|
|
||||||
|
var sessionTmpl = template.Must(template.Must(BaseTemplate.Clone()).Parse(`
|
||||||
|
{{define "title"}}Session {{.ID}}{{end}}
|
||||||
|
{{define "body"}}
|
||||||
|
From: <b>{{template "cachelink" .Cache.ID}}</b><br>
|
||||||
|
<h2>Views</h2>
|
||||||
|
<ul>{{range .Views}}<li>{{.Name}} is {{template "viewlink" .ID}} in {{.Folder}}</li>{{end}}</ul>
|
||||||
|
<h2>Files</h2>
|
||||||
|
<ul>{{range .Files}}<li>{{template "filelink" .}}</li>{{end}}</ul>
|
||||||
|
{{end}}
|
||||||
|
`))
|
||||||
|
|
||||||
|
var viewTmpl = template.Must(template.Must(BaseTemplate.Clone()).Parse(`
|
||||||
|
{{define "title"}}View {{.ID}}{{end}}
|
||||||
|
{{define "body"}}
|
||||||
|
Name: <b>{{.Name}}</b><br>
|
||||||
|
Folder: <b>{{.Folder}}</b><br>
|
||||||
|
From: <b>{{template "sessionlink" .Session.ID}}</b><br>
|
||||||
|
<h2>Environment</h2>
|
||||||
|
<ul>{{range .Env}}<li>{{.}}</li>{{end}}</ul>
|
||||||
|
{{end}}
|
||||||
|
`))
|
||||||
|
|
||||||
|
var fileTmpl = template.Must(template.Must(BaseTemplate.Clone()).Parse(`
|
||||||
|
{{define "title"}}File {{.Hash}}{{end}}
|
||||||
|
{{define "body"}}
|
||||||
|
From: <b>{{template "sessionlink" .Session.ID}}</b><br>
|
||||||
|
URI: <b>{{.URI}}</b><br>
|
||||||
|
Hash: <b>{{.Hash}}</b><br>
|
||||||
|
Error: <b>{{.Error}}</b><br>
|
||||||
|
<h3>Contents</h3>
|
||||||
|
<pre>{{.Data}}</pre>
|
||||||
{{end}}
|
{{end}}
|
||||||
`))
|
`))
|
||||||
|
|
Loading…
Reference in New Issue