diff --git a/internal/lsp/cache/cache.go b/internal/lsp/cache/cache.go index cdb1d490..2f0e14d8 100644 --- a/internal/lsp/cache/cache.go +++ b/internal/lsp/cache/cache.go @@ -8,31 +8,43 @@ import ( "crypto/sha1" "fmt" "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/xlog" "golang.org/x/tools/internal/span" ) func New() source.Cache { - return &cache{ + index := atomic.AddInt64(&cacheIndex, 1) + c := &cache{ + id: strconv.FormatInt(index, 10), fset: token.NewFileSet(), } + debug.AddCache(debugCache{c}) + return c } type cache struct { nativeFileSystem + id string fset *token.FileSet } func (c *cache) NewSession(log xlog.Logger) source.Session { - return &session{ + index := atomic.AddInt64(&sessionIndex, 1) + s := &session{ cache: c, + id: strconv.FormatInt(index, 10), log: log, overlays: make(map[span.URI]*source.FileContent), filesWatchMap: NewWatchMap(), } + debug.AddSession(debugSession{s}) + return s } func (c *cache) FileSet() *token.FileSet { @@ -44,3 +56,10 @@ func hashContents(contents []byte) string { // This hash is used for internal identity detection only 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 } diff --git a/internal/lsp/cache/session.go b/internal/lsp/cache/session.go index 57735b2b..920eac24 100644 --- a/internal/lsp/cache/session.go +++ b/internal/lsp/cache/session.go @@ -8,9 +8,13 @@ import ( "context" "fmt" "os" + "sort" + "strconv" "strings" "sync" + "sync/atomic" + "golang.org/x/tools/internal/lsp/debug" "golang.org/x/tools/internal/lsp/source" "golang.org/x/tools/internal/lsp/xlog" "golang.org/x/tools/internal/span" @@ -18,6 +22,7 @@ import ( type session struct { cache *cache + id string // the logger to use to communicate back with the client log xlog.Logger @@ -40,6 +45,7 @@ func (s *session) Shutdown(ctx context.Context) { } s.views = nil s.viewMap = nil + debug.DropSession(debugSession{s}) } 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 { + index := atomic.AddInt64(&viewIndex, 1) s.viewMu.Lock() defer s.viewMu.Unlock() ctx := context.Background() backgroundCtx, cancel := context.WithCancel(ctx) v := &view{ session: s, + id: strconv.FormatInt(index, 10), baseCtx: ctx, backgroundCtx: backgroundCtx, cancel: cancel, @@ -73,6 +81,7 @@ func (s *session) NewView(name string, folder span.URI) source.View { s.views = append(s.views, v) // we always need to drop the view map s.viewMap = make(map[span.URI]source.View) + debug.AddView(debugView{v}) return v } @@ -225,3 +234,58 @@ func (s *session) buildOverlay() map[string][]byte { } 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, + } +} diff --git a/internal/lsp/cache/view.go b/internal/lsp/cache/view.go index 96983b1c..b2bb3e61 100644 --- a/internal/lsp/cache/view.go +++ b/internal/lsp/cache/view.go @@ -15,12 +15,14 @@ import ( "sync" "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/span" ) type view struct { session *session + id string // mu protects all mutable state of the view. mu sync.Mutex @@ -157,6 +159,7 @@ func (v *view) shutdown(context.Context) { v.cancel() v.cancel = nil } + debug.DropView(debugView{v}) } // 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) } } + +type debugView struct{ *view } + +func (v debugView) ID() string { return v.id } +func (v debugView) Session() debug.Session { return debugSession{v.session} } diff --git a/internal/lsp/debug/serve.go b/internal/lsp/debug/serve.go index c5a1bbdf..3ea564e0 100644 --- a/internal/lsp/debug/serve.go +++ b/internal/lsp/debug/serve.go @@ -7,29 +7,204 @@ package debug import ( "bytes" "context" + "go/token" "html/template" "log" "net" "net/http" _ "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() { - http.HandleFunc("/", Render(mainTmpl, nil)) + http.HandleFunc("/", Render(mainTmpl, func(*http.Request) interface{} { return data })) 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)) } +// 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{} { buf := &bytes.Buffer{} PrintVersionInfo(buf, true, HTML) 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. // It also logs the port the server starts on, to allow for :0 auto assigned // ports. func Serve(ctx context.Context, addr string) error { + mu.Lock() + defer mu.Unlock() if addr == "" { 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(` -{{template "title"}} +{{template "title" .}}