github.com/drellem2/pogo@v0.0.0-20240503070746-2c2b76da329a/internal/plugins/search/search_impl.go (about) 1 package search 2 3 import ( 4 "encoding/json" 5 "net/url" 6 "os" 7 "path/filepath" 8 "strconv" 9 10 "github.com/fsnotify/fsnotify" 11 "github.com/hashicorp/go-hclog" 12 "github.com/hashicorp/go-plugin" 13 14 pogoPlugin "github.com/drellem2/pogo/pkg/plugin" 15 ) 16 17 const pogoDir = ".pogo" 18 const searchDir = "search" 19 20 // API Version for this plugin 21 const version = "0.0.1" 22 23 const UseWatchers = true 24 25 type BasicSearch struct { 26 logger hclog.Logger 27 projects map[string]IndexedProject 28 watcher *fsnotify.Watcher 29 updater *ProjectUpdater 30 } 31 32 // Input to an "Execute" call should be a serialized SearchRequest 33 type SearchRequest struct { 34 // Values: "search" or "files" 35 Type string `json:"type"` 36 ProjectRoot string `json:"projectRoot"` 37 // Command timeout duration - only for 'search'-type requests 38 Duration string `json:"string"` 39 Data string `json:"data"` 40 } 41 42 type SearchResponse struct { 43 Index IndexedProject `json:"index"` 44 Results SearchResults `json:"results"` 45 Error string `json:"error"` 46 } 47 48 type ErrorResponse struct { 49 ErrorCode int `json:"errorCode"` 50 Error string `json:"error"` 51 } 52 53 func New() func() (pogoPlugin.IPogoPlugin, error) { 54 return func() (pogoPlugin.IPogoPlugin, error) { 55 return createBasicSearch(), nil 56 } 57 } 58 59 func clean(path string) string { 60 // Append a trailing delimiter if it doesn't exist 61 p := filepath.Clean(path) 62 if p[len(p)-1] != filepath.Separator { 63 p += string(filepath.Separator) 64 } 65 return p 66 } 67 68 func (g *BasicSearch) printSearchResponse(response SearchResponse) string { 69 // Instead of marshalling the obect, write code to go through all fields 70 // and concatenate them into a string. 71 var str string 72 str += "Index: " + response.Index.Root + "\n" 73 str += "Paths: " + "\n" 74 for _, path := range response.Index.Paths { 75 str += path + "\n" 76 } 77 str += "Results: " + "\n" 78 for _, result := range response.Results.Files { 79 str += "\t" + result.Path + "\n" 80 for _, match := range result.Matches { 81 // Convert match.Content bytes to string 82 var lineStr = strconv.FormatUint(uint64(match.Line), 10) 83 str += "\t\t" + lineStr + "\n" 84 if len(match.Content) > 0 { 85 // str += "\t\t" + string(match.Content) + "\n" 86 str += "\t\t" + match.Content + "\n" 87 } else { 88 str += "\t\t" + "No content" + "\n" 89 } 90 } 91 } 92 str += "Error: " + response.Error + "\n" 93 return str 94 } 95 96 func (g *BasicSearch) errorResponse(code int, message string) string { 97 resp := ErrorResponse{ErrorCode: code, Error: message} 98 bytes, err := json.Marshal(&resp) 99 if err != nil { 100 g.logger.Error("Error writing error response") 101 panic(err) 102 } 103 return url.QueryEscape(string(bytes)) 104 } 105 106 func (g *BasicSearch) searchResponse(index *IndexedProject, results *SearchResults) string { 107 var response SearchResponse 108 if index == nil { 109 indexedProject := IndexedProject{Root: "", Paths: []string{}} 110 response.Index = indexedProject 111 } else { 112 response.Index = *index 113 } 114 if results == nil { 115 g.logger.Info("Search response was nil") 116 searchResults := SearchResults{} 117 response.Results = searchResults 118 } else { 119 response.Results = *results 120 } 121 response.Error = "" 122 123 bytes, err := json.Marshal(&response) 124 if err != nil { 125 g.logger.Error("Error writing search response") 126 return g.errorResponse(500, "Error writing search response") 127 } 128 return url.QueryEscape(string(bytes)) 129 } 130 131 func (g *BasicSearch) Info() *pogoPlugin.PluginInfoRes { 132 g.logger.Debug("Returning version %s", version) 133 return &pogoPlugin.PluginInfoRes{Version: version} 134 } 135 136 // Executes a command sent to this plugin. 137 func (g *BasicSearch) Execute(encodedReq string) string { 138 g.logger.Debug("Executing request.") 139 req, err2 := url.QueryUnescape(encodedReq) 140 if err2 != nil { 141 g.logger.Error("500 Could not query decode request.", "error", err2) 142 return g.errorResponse(500, "Could not query decode request.") 143 } 144 var searchRequest SearchRequest 145 err := json.Unmarshal([]byte(req), &searchRequest) 146 if err != nil { 147 g.logger.Info("400 Invalid request.", "error", err) 148 return g.errorResponse(400, "Invalid request.") 149 } 150 151 switch reqType := searchRequest.Type; reqType { 152 case "search": 153 searchRequest.ProjectRoot = clean(searchRequest.ProjectRoot) 154 results, err := g.Search(searchRequest.ProjectRoot, 155 searchRequest.Data, searchRequest.Duration) 156 if err != nil { 157 g.logger.Error("500 Error executing search.", "error", err) 158 return g.errorResponse(500, "Error executing search.") 159 } 160 return g.searchResponse(nil, results) 161 case "files": 162 searchRequest.ProjectRoot = clean(searchRequest.ProjectRoot) 163 proj, err3 := g.GetFiles(searchRequest.ProjectRoot) 164 if err3 != nil { 165 g.logger.Error("500 Error retrieving files.", "error", err3) 166 return g.errorResponse(500, "Error retrieving files.") 167 } 168 return g.searchResponse(proj, nil) 169 default: 170 g.logger.Info("404 Unknown request type.", "type", searchRequest.Type) 171 return g.errorResponse(404, "Unknown request type.") 172 } 173 174 } 175 176 func (g *BasicSearch) ProcessProject(req *pogoPlugin.IProcessProjectReq) error { 177 g.logger.Info("Processing project %s", (*req).Path()) 178 proj, err := g.Load((*req).Path()) 179 if err != nil { 180 g.logger.Error("Error processing project", "error", err) 181 } 182 if err != nil || len(proj.Paths) == 0 { 183 go g.Index(req) 184 } 185 return nil 186 } 187 188 func (g *BasicSearch) Close() { 189 g.watcher.Close() 190 } 191 192 // handshakeConfigs are used to just do a basic handshake betw1een 193 // a plugin and host. If the handshake fails, a user friendly error is shown. 194 // This prevents users from executing bad plugins or executing a plugin 195 // directory. It is a UX feature, not a security feature. 196 var handshakeConfig = plugin.HandshakeConfig{ 197 ProtocolVersion: 2, 198 MagicCookieKey: "SEARCH_PLUGIN", 199 MagicCookieValue: "93f6bc9f97c03ed00fa85c904aca15a92752e549", 200 } 201 202 // Ensure's plugin directory exists in project config 203 // Returns full path of search dir 204 func (p *IndexedProject) makeSearchDir() (string, error) { 205 fullSearchDir := filepath.Join(p.Root, pogoDir, searchDir) 206 err := os.MkdirAll(fullSearchDir, os.ModePerm) 207 if err != nil { 208 return "", err 209 } 210 return fullSearchDir, nil 211 } 212 213 func createBasicSearch() *BasicSearch { 214 logger := hclog.New(&hclog.LoggerOptions{ 215 Level: hclog.Info, 216 Output: os.Stderr, 217 JSONFormat: true, 218 }) 219 220 watcher, err := fsnotify.NewWatcher() 221 if err != nil { 222 logger.Error("Could not create file watcher. Index will run frequently.") 223 } 224 225 basicSearch := &BasicSearch{ 226 logger: logger, 227 projects: make(map[string]IndexedProject), 228 watcher: watcher, 229 updater: nil, 230 } 231 basicSearch.updater = basicSearch.newProjectUpdater() 232 233 if UseWatchers && watcher != nil { 234 go func() { 235 for { 236 select { 237 case event, ok := <-watcher.Events: 238 if !ok { 239 logger.Warn("Not ok") 240 logger.Warn(event.String()) 241 return 242 } 243 if event.Has(fsnotify.Create) || event.Has(fsnotify.Remove) || event.Has(fsnotify.Rename) { 244 logger.Info("File update: ", event) 245 basicSearch.ReIndex(event.Name) 246 } 247 case err, ok := <-watcher.Errors: 248 249 if !ok { 250 return 251 } 252 logger.Error("File watcher error: %v", err) 253 } 254 } 255 }() 256 } 257 return basicSearch 258 } 259 260 // This is how to serve a plugin remotely 261 // func main() { 262 // gob.Register(pogoPlugin.ProcessProjectReq{}) 263 264 // basicSearch := createBasicSearch() 265 // defer basicSearch.watcher.Close() 266 267 // // pluginMap is the map of plugins we can dispense. 268 // var pluginMap = map[string]plugin.Plugin{ 269 // "basicSearch": &pogoPlugin.PogoPlugin{Impl: basicSearch}, 270 // } 271 272 // plugin.Serve(&plugin.ServeConfig{ 273 // HandshakeConfig: handshakeConfig, 274 // Plugins: pluginMap, 275 // }) 276 // }