github.com/jhump/golang-x-tools@v0.0.0-20220218190644-4958d6d39439/internal/lsp/fake/workdir.go (about) 1 // Copyright 2020 The Go Authors. All rights reserved. 2 // Use of this source code is governed by a BSD-style 3 // license that can be found in the LICENSE file. 4 5 package fake 6 7 import ( 8 "bytes" 9 "context" 10 "crypto/sha256" 11 "fmt" 12 "io/ioutil" 13 "os" 14 "path/filepath" 15 "runtime" 16 "strings" 17 "sync" 18 "time" 19 20 "github.com/jhump/golang-x-tools/internal/lsp/protocol" 21 "github.com/jhump/golang-x-tools/internal/span" 22 errors "golang.org/x/xerrors" 23 ) 24 25 // FileEvent wraps the protocol.FileEvent so that it can be associated with a 26 // workdir-relative path. 27 type FileEvent struct { 28 Path, Content string 29 ProtocolEvent protocol.FileEvent 30 } 31 32 // RelativeTo is a helper for operations relative to a given directory. 33 type RelativeTo string 34 35 // AbsPath returns an absolute filesystem path for the workdir-relative path. 36 func (r RelativeTo) AbsPath(path string) string { 37 fp := filepath.FromSlash(path) 38 if filepath.IsAbs(fp) { 39 return fp 40 } 41 return filepath.Join(string(r), filepath.FromSlash(path)) 42 } 43 44 // RelPath returns a '/'-encoded path relative to the working directory (or an 45 // absolute path if the file is outside of workdir) 46 func (r RelativeTo) RelPath(fp string) string { 47 root := string(r) 48 if rel, err := filepath.Rel(root, fp); err == nil && !strings.HasPrefix(rel, "..") { 49 return filepath.ToSlash(rel) 50 } 51 return filepath.ToSlash(fp) 52 } 53 54 func writeTxtar(txt string, rel RelativeTo) error { 55 files := UnpackTxt(txt) 56 for name, data := range files { 57 if err := WriteFileData(name, data, rel); err != nil { 58 return errors.Errorf("writing to workdir: %w", err) 59 } 60 } 61 return nil 62 } 63 64 // WriteFileData writes content to the relative path, replacing the special 65 // token $SANDBOX_WORKDIR with the relative root given by rel. 66 func WriteFileData(path string, content []byte, rel RelativeTo) error { 67 content = bytes.ReplaceAll(content, []byte("$SANDBOX_WORKDIR"), []byte(rel)) 68 fp := rel.AbsPath(path) 69 if err := os.MkdirAll(filepath.Dir(fp), 0755); err != nil { 70 return errors.Errorf("creating nested directory: %w", err) 71 } 72 backoff := 1 * time.Millisecond 73 for { 74 err := ioutil.WriteFile(fp, []byte(content), 0644) 75 if err != nil { 76 if isWindowsErrLockViolation(err) { 77 time.Sleep(backoff) 78 backoff *= 2 79 continue 80 } 81 return errors.Errorf("writing %q: %w", path, err) 82 } 83 return nil 84 } 85 } 86 87 // isWindowsErrLockViolation reports whether err is ERROR_LOCK_VIOLATION 88 // on Windows. 89 var isWindowsErrLockViolation = func(err error) bool { return false } 90 91 // Workdir is a temporary working directory for tests. It exposes file 92 // operations in terms of relative paths, and fakes file watching by triggering 93 // events on file operations. 94 type Workdir struct { 95 RelativeTo 96 97 watcherMu sync.Mutex 98 watchers []func(context.Context, []FileEvent) 99 100 fileMu sync.Mutex 101 files map[string]string 102 } 103 104 // NewWorkdir writes the txtar-encoded file data in txt to dir, and returns a 105 // Workir for operating on these files using 106 func NewWorkdir(dir string) *Workdir { 107 return &Workdir{RelativeTo: RelativeTo(dir)} 108 } 109 110 func hashFile(data []byte) string { 111 return fmt.Sprintf("%x", sha256.Sum256(data)) 112 } 113 114 func (w *Workdir) writeInitialFiles(files map[string][]byte) error { 115 w.files = map[string]string{} 116 for name, data := range files { 117 w.files[name] = hashFile(data) 118 if err := WriteFileData(name, data, w.RelativeTo); err != nil { 119 return errors.Errorf("writing to workdir: %w", err) 120 } 121 } 122 return nil 123 } 124 125 // RootURI returns the root URI for this working directory of this scratch 126 // environment. 127 func (w *Workdir) RootURI() protocol.DocumentURI { 128 return toURI(string(w.RelativeTo)) 129 } 130 131 // AddWatcher registers the given func to be called on any file change. 132 func (w *Workdir) AddWatcher(watcher func(context.Context, []FileEvent)) { 133 w.watcherMu.Lock() 134 w.watchers = append(w.watchers, watcher) 135 w.watcherMu.Unlock() 136 } 137 138 // URI returns the URI to a the workdir-relative path. 139 func (w *Workdir) URI(path string) protocol.DocumentURI { 140 return toURI(w.AbsPath(path)) 141 } 142 143 // URIToPath converts a uri to a workdir-relative path (or an absolute path, 144 // if the uri is outside of the workdir). 145 func (w *Workdir) URIToPath(uri protocol.DocumentURI) string { 146 fp := uri.SpanURI().Filename() 147 return w.RelPath(fp) 148 } 149 150 func toURI(fp string) protocol.DocumentURI { 151 return protocol.DocumentURI(span.URIFromPath(fp)) 152 } 153 154 // ReadFile reads a text file specified by a workdir-relative path. 155 func (w *Workdir) ReadFile(path string) (string, error) { 156 backoff := 1 * time.Millisecond 157 for { 158 b, err := ioutil.ReadFile(w.AbsPath(path)) 159 if err != nil { 160 if runtime.GOOS == "plan9" && strings.HasSuffix(err.Error(), " exclusive use file already open") { 161 // Plan 9 enforces exclusive access to locked files. 162 // Give the owner time to unlock it and retry. 163 time.Sleep(backoff) 164 backoff *= 2 165 continue 166 } 167 return "", err 168 } 169 return string(b), nil 170 } 171 } 172 173 func (w *Workdir) RegexpRange(path, re string) (Pos, Pos, error) { 174 content, err := w.ReadFile(path) 175 if err != nil { 176 return Pos{}, Pos{}, err 177 } 178 return regexpRange(content, re) 179 } 180 181 // RegexpSearch searches the file corresponding to path for the first position 182 // matching re. 183 func (w *Workdir) RegexpSearch(path string, re string) (Pos, error) { 184 content, err := w.ReadFile(path) 185 if err != nil { 186 return Pos{}, err 187 } 188 start, _, err := regexpRange(content, re) 189 return start, err 190 } 191 192 // ChangeFilesOnDisk executes the given on-disk file changes in a batch, 193 // simulating the action of changing branches outside of an editor. 194 func (w *Workdir) ChangeFilesOnDisk(ctx context.Context, events []FileEvent) error { 195 for _, e := range events { 196 switch e.ProtocolEvent.Type { 197 case protocol.Deleted: 198 fp := w.AbsPath(e.Path) 199 if err := os.Remove(fp); err != nil { 200 return errors.Errorf("removing %q: %w", e.Path, err) 201 } 202 case protocol.Changed, protocol.Created: 203 if _, err := w.writeFile(ctx, e.Path, e.Content); err != nil { 204 return err 205 } 206 } 207 } 208 w.sendEvents(ctx, events) 209 return nil 210 } 211 212 // RemoveFile removes a workdir-relative file path. 213 func (w *Workdir) RemoveFile(ctx context.Context, path string) error { 214 fp := w.AbsPath(path) 215 if err := os.RemoveAll(fp); err != nil { 216 return errors.Errorf("removing %q: %w", path, err) 217 } 218 w.fileMu.Lock() 219 defer w.fileMu.Unlock() 220 221 evts := []FileEvent{{ 222 Path: path, 223 ProtocolEvent: protocol.FileEvent{ 224 URI: w.URI(path), 225 Type: protocol.Deleted, 226 }, 227 }} 228 w.sendEvents(ctx, evts) 229 delete(w.files, path) 230 return nil 231 } 232 233 func (w *Workdir) sendEvents(ctx context.Context, evts []FileEvent) { 234 if len(evts) == 0 { 235 return 236 } 237 w.watcherMu.Lock() 238 watchers := make([]func(context.Context, []FileEvent), len(w.watchers)) 239 copy(watchers, w.watchers) 240 w.watcherMu.Unlock() 241 for _, w := range watchers { 242 w(ctx, evts) 243 } 244 } 245 246 // WriteFiles writes the text file content to workdir-relative paths. 247 // It batches notifications rather than sending them consecutively. 248 func (w *Workdir) WriteFiles(ctx context.Context, files map[string]string) error { 249 var evts []FileEvent 250 for filename, content := range files { 251 evt, err := w.writeFile(ctx, filename, content) 252 if err != nil { 253 return err 254 } 255 evts = append(evts, evt) 256 } 257 w.sendEvents(ctx, evts) 258 return nil 259 } 260 261 // WriteFile writes text file content to a workdir-relative path. 262 func (w *Workdir) WriteFile(ctx context.Context, path, content string) error { 263 evt, err := w.writeFile(ctx, path, content) 264 if err != nil { 265 return err 266 } 267 w.sendEvents(ctx, []FileEvent{evt}) 268 return nil 269 } 270 271 func (w *Workdir) writeFile(ctx context.Context, path, content string) (FileEvent, error) { 272 fp := w.AbsPath(path) 273 _, err := os.Stat(fp) 274 if err != nil && !os.IsNotExist(err) { 275 return FileEvent{}, errors.Errorf("checking if %q exists: %w", path, err) 276 } 277 var changeType protocol.FileChangeType 278 if os.IsNotExist(err) { 279 changeType = protocol.Created 280 } else { 281 changeType = protocol.Changed 282 } 283 if err := WriteFileData(path, []byte(content), w.RelativeTo); err != nil { 284 return FileEvent{}, err 285 } 286 return FileEvent{ 287 Path: path, 288 ProtocolEvent: protocol.FileEvent{ 289 URI: w.URI(path), 290 Type: changeType, 291 }, 292 }, nil 293 } 294 295 // listFiles lists files in the given directory, returning a map of relative 296 // path to modification time. 297 func (w *Workdir) listFiles(dir string) (map[string]string, error) { 298 files := make(map[string]string) 299 absDir := w.AbsPath(dir) 300 if err := filepath.Walk(absDir, func(fp string, info os.FileInfo, err error) error { 301 if err != nil { 302 return err 303 } 304 if info.IsDir() { 305 return nil 306 } 307 path := w.RelPath(fp) 308 data, err := ioutil.ReadFile(fp) 309 if err != nil { 310 return err 311 } 312 files[path] = hashFile(data) 313 return nil 314 }); err != nil { 315 return nil, err 316 } 317 return files, nil 318 } 319 320 // CheckForFileChanges walks the working directory and checks for any files 321 // that have changed since the last poll. 322 func (w *Workdir) CheckForFileChanges(ctx context.Context) error { 323 evts, err := w.pollFiles() 324 if err != nil { 325 return err 326 } 327 w.sendEvents(ctx, evts) 328 return nil 329 } 330 331 // pollFiles updates w.files and calculates FileEvents corresponding to file 332 // state changes since the last poll. It does not call sendEvents. 333 func (w *Workdir) pollFiles() ([]FileEvent, error) { 334 w.fileMu.Lock() 335 defer w.fileMu.Unlock() 336 337 files, err := w.listFiles(".") 338 if err != nil { 339 return nil, err 340 } 341 var evts []FileEvent 342 // Check which files have been added or modified. 343 for path, hash := range files { 344 oldhash, ok := w.files[path] 345 delete(w.files, path) 346 var typ protocol.FileChangeType 347 switch { 348 case !ok: 349 typ = protocol.Created 350 case oldhash != hash: 351 typ = protocol.Changed 352 default: 353 continue 354 } 355 evts = append(evts, FileEvent{ 356 Path: path, 357 ProtocolEvent: protocol.FileEvent{ 358 URI: w.URI(path), 359 Type: typ, 360 }, 361 }) 362 } 363 // Any remaining files must have been deleted. 364 for path := range w.files { 365 evts = append(evts, FileEvent{ 366 Path: path, 367 ProtocolEvent: protocol.FileEvent{ 368 URI: w.URI(path), 369 Type: protocol.Deleted, 370 }, 371 }) 372 } 373 w.files = files 374 return evts, nil 375 }