golang.org/x/tools/gopls@v0.15.3/internal/test/integration/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/fs" 13 "os" 14 "path/filepath" 15 "runtime" 16 "sort" 17 "strings" 18 "sync" 19 "time" 20 21 "golang.org/x/tools/gopls/internal/protocol" 22 "golang.org/x/tools/internal/robustio" 23 ) 24 25 // RelativeTo is a helper for operations relative to a given directory. 26 type RelativeTo string 27 28 // AbsPath returns an absolute filesystem path for the workdir-relative path. 29 func (r RelativeTo) AbsPath(path string) string { 30 fp := filepath.FromSlash(path) 31 if filepath.IsAbs(fp) { 32 return fp 33 } 34 return filepath.Join(string(r), filepath.FromSlash(path)) 35 } 36 37 // RelPath returns a '/'-encoded path relative to the working directory (or an 38 // absolute path if the file is outside of workdir) 39 func (r RelativeTo) RelPath(fp string) string { 40 root := string(r) 41 if rel, err := filepath.Rel(root, fp); err == nil && !strings.HasPrefix(rel, "..") { 42 return filepath.ToSlash(rel) 43 } 44 return filepath.ToSlash(fp) 45 } 46 47 // writeFileData writes content to the relative path, replacing the special 48 // token $SANDBOX_WORKDIR with the relative root given by rel. It does not 49 // trigger any file events. 50 func writeFileData(path string, content []byte, rel RelativeTo) error { 51 content = bytes.ReplaceAll(content, []byte("$SANDBOX_WORKDIR"), []byte(rel)) 52 fp := rel.AbsPath(path) 53 if err := os.MkdirAll(filepath.Dir(fp), 0755); err != nil { 54 return fmt.Errorf("creating nested directory: %w", err) 55 } 56 backoff := 1 * time.Millisecond 57 for { 58 err := os.WriteFile(fp, content, 0644) 59 if err != nil { 60 // This lock file violation is not handled by the robustio package, as it 61 // indicates a real race condition that could be avoided. 62 if isWindowsErrLockViolation(err) { 63 time.Sleep(backoff) 64 backoff *= 2 65 continue 66 } 67 return fmt.Errorf("writing %q: %w", path, err) 68 } 69 return nil 70 } 71 } 72 73 // isWindowsErrLockViolation reports whether err is ERROR_LOCK_VIOLATION 74 // on Windows. 75 var isWindowsErrLockViolation = func(err error) bool { return false } 76 77 // Workdir is a temporary working directory for tests. It exposes file 78 // operations in terms of relative paths, and fakes file watching by triggering 79 // events on file operations. 80 type Workdir struct { 81 RelativeTo 82 83 watcherMu sync.Mutex 84 watchers []func(context.Context, []protocol.FileEvent) 85 86 fileMu sync.Mutex 87 // File identities we know about, for the purpose of detecting changes. 88 // 89 // Since files is only used for detecting _changes_, we are tolerant of 90 // fileIDs that may have hash and mtime coming from different states of the 91 // file: if either are out of sync, then the next poll should detect a 92 // discrepancy. It is OK if we detect too many changes, but not OK if we miss 93 // changes. 94 // 95 // For that matter, this mechanism for detecting changes can still be flaky 96 // on platforms where mtime is very coarse (such as older versions of WSL). 97 // It would be much better to use a proper fs event library, but we can't 98 // currently import those into x/tools. 99 // 100 // TODO(golang/go#52284): replace this polling mechanism with a 101 // cross-platform library for filesystem notifications. 102 files map[string]fileID 103 } 104 105 // NewWorkdir writes the txtar-encoded file data in txt to dir, and returns a 106 // Workir for operating on these files using 107 func NewWorkdir(dir string, files map[string][]byte) (*Workdir, error) { 108 w := &Workdir{RelativeTo: RelativeTo(dir)} 109 for name, data := range files { 110 if err := writeFileData(name, data, w.RelativeTo); err != nil { 111 return nil, fmt.Errorf("writing to workdir: %w", err) 112 } 113 } 114 _, err := w.pollFiles() // poll files to populate the files map. 115 return w, err 116 } 117 118 // fileID identifies a file version on disk. 119 type fileID struct { 120 mtime time.Time 121 hash string // empty if mtime is old enough to be reliable; otherwise a file digest 122 } 123 124 func hashFile(data []byte) string { 125 return fmt.Sprintf("%x", sha256.Sum256(data)) 126 } 127 128 // RootURI returns the root URI for this working directory of this scratch 129 // environment. 130 func (w *Workdir) RootURI() protocol.DocumentURI { 131 return protocol.URIFromPath(string(w.RelativeTo)) 132 } 133 134 // AddWatcher registers the given func to be called on any file change. 135 func (w *Workdir) AddWatcher(watcher func(context.Context, []protocol.FileEvent)) { 136 w.watcherMu.Lock() 137 w.watchers = append(w.watchers, watcher) 138 w.watcherMu.Unlock() 139 } 140 141 // URI returns the URI to a the workdir-relative path. 142 func (w *Workdir) URI(path string) protocol.DocumentURI { 143 return protocol.URIFromPath(w.AbsPath(path)) 144 } 145 146 // URIToPath converts a uri to a workdir-relative path (or an absolute path, 147 // if the uri is outside of the workdir). 148 func (w *Workdir) URIToPath(uri protocol.DocumentURI) string { 149 return w.RelPath(uri.Path()) 150 } 151 152 // ReadFile reads a text file specified by a workdir-relative path. 153 func (w *Workdir) ReadFile(path string) ([]byte, error) { 154 backoff := 1 * time.Millisecond 155 for { 156 b, err := os.ReadFile(w.AbsPath(path)) 157 if err != nil { 158 if runtime.GOOS == "plan9" && strings.HasSuffix(err.Error(), " exclusive use file already open") { 159 // Plan 9 enforces exclusive access to locked files. 160 // Give the owner time to unlock it and retry. 161 time.Sleep(backoff) 162 backoff *= 2 163 continue 164 } 165 return nil, err 166 } 167 return b, nil 168 } 169 } 170 171 // RegexpSearch searches the file corresponding to path for the first position 172 // matching re. 173 func (w *Workdir) RegexpSearch(path string, re string) (protocol.Location, error) { 174 content, err := w.ReadFile(path) 175 if err != nil { 176 return protocol.Location{}, err 177 } 178 mapper := protocol.NewMapper(w.URI(path), content) 179 return regexpLocation(mapper, re) 180 } 181 182 // RemoveFile removes a workdir-relative file path and notifies watchers of the 183 // change. 184 func (w *Workdir) RemoveFile(ctx context.Context, path string) error { 185 fp := w.AbsPath(path) 186 if err := robustio.RemoveAll(fp); err != nil { 187 return fmt.Errorf("removing %q: %w", path, err) 188 } 189 190 return w.CheckForFileChanges(ctx) 191 } 192 193 // WriteFiles writes the text file content to workdir-relative paths and 194 // notifies watchers of the changes. 195 func (w *Workdir) WriteFiles(ctx context.Context, files map[string]string) error { 196 for path, content := range files { 197 fp := w.AbsPath(path) 198 _, err := os.Stat(fp) 199 if err != nil && !os.IsNotExist(err) { 200 return fmt.Errorf("checking if %q exists: %w", path, err) 201 } 202 if err := writeFileData(path, []byte(content), w.RelativeTo); err != nil { 203 return err 204 } 205 } 206 return w.CheckForFileChanges(ctx) 207 } 208 209 // WriteFile writes text file content to a workdir-relative path and notifies 210 // watchers of the change. 211 func (w *Workdir) WriteFile(ctx context.Context, path, content string) error { 212 return w.WriteFiles(ctx, map[string]string{path: content}) 213 } 214 215 // RenameFile performs an on disk-renaming of the workdir-relative oldPath to 216 // workdir-relative newPath, and notifies watchers of the changes. 217 // 218 // oldPath must either be a regular file or in the same directory as newPath. 219 func (w *Workdir) RenameFile(ctx context.Context, oldPath, newPath string) error { 220 oldAbs := w.AbsPath(oldPath) 221 newAbs := w.AbsPath(newPath) 222 223 // For os.Rename, “OS-specific restrictions may apply when oldpath and newpath 224 // are in different directories.” If that applies here, we may fall back to 225 // ReadFile, WriteFile, and RemoveFile to perform the rename non-atomically. 226 // 227 // However, the fallback path only works for regular files: renaming a 228 // directory would be much more complex and isn't needed for our tests. 229 fallbackOk := false 230 if filepath.Dir(oldAbs) != filepath.Dir(newAbs) { 231 fi, err := os.Stat(oldAbs) 232 if err == nil && !fi.Mode().IsRegular() { 233 return &os.PathError{ 234 Op: "RenameFile", 235 Path: oldPath, 236 Err: fmt.Errorf("%w: file is not regular and not in the same directory as %s", os.ErrInvalid, newPath), 237 } 238 } 239 fallbackOk = true 240 } 241 242 var renameErr error 243 const debugFallback = false 244 if fallbackOk && debugFallback { 245 renameErr = fmt.Errorf("%w: debugging fallback path", os.ErrInvalid) 246 } else { 247 renameErr = robustio.Rename(oldAbs, newAbs) 248 } 249 if renameErr != nil { 250 if !fallbackOk { 251 return renameErr // The OS-specific Rename restrictions do not apply. 252 } 253 254 content, err := w.ReadFile(oldPath) 255 if err != nil { 256 // If we can't even read the file, the error from Rename may be accurate. 257 return renameErr 258 } 259 fi, err := os.Stat(newAbs) 260 if err == nil { 261 if fi.IsDir() { 262 // “If newpath already exists and is not a directory, Rename replaces it.” 263 // But if it is a directory, maybe not? 264 return renameErr 265 } 266 // On most platforms, Rename replaces the named file with a new file, 267 // rather than overwriting the existing file it in place. Mimic that 268 // behavior here. 269 if err := robustio.RemoveAll(newAbs); err != nil { 270 // Maybe we don't have permission to replace newPath? 271 return renameErr 272 } 273 } else if !os.IsNotExist(err) { 274 // If the destination path already exists or there is some problem with it, 275 // the error from Rename may be accurate. 276 return renameErr 277 } 278 if writeErr := writeFileData(newPath, content, w.RelativeTo); writeErr != nil { 279 // At this point we have tried to actually write the file. 280 // If it still doesn't exist, assume that the error from Rename was accurate: 281 // for example, maybe we don't have permission to create the new path. 282 // Otherwise, return the error from the write, which may indicate some 283 // other problem (such as a full disk). 284 if _, statErr := os.Stat(newAbs); !os.IsNotExist(statErr) { 285 return writeErr 286 } 287 return renameErr 288 } 289 if err := robustio.RemoveAll(oldAbs); err != nil { 290 // If we failed to remove the old file, that may explain the Rename error too. 291 // Make a best effort to back out the write to the new path. 292 robustio.RemoveAll(newAbs) 293 return renameErr 294 } 295 } 296 297 return w.CheckForFileChanges(ctx) 298 } 299 300 // ListFiles returns a new sorted list of the relative paths of files in dir, 301 // recursively. 302 func (w *Workdir) ListFiles(dir string) ([]string, error) { 303 absDir := w.AbsPath(dir) 304 var paths []string 305 if err := filepath.Walk(absDir, func(fp string, info os.FileInfo, err error) error { 306 if err != nil { 307 return err 308 } 309 if info.Mode()&(fs.ModeDir|fs.ModeSymlink) == 0 { 310 paths = append(paths, w.RelPath(fp)) 311 } 312 return nil 313 }); err != nil { 314 return nil, err 315 } 316 sort.Strings(paths) 317 return paths, 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 if len(evts) == 0 { 328 return nil 329 } 330 w.watcherMu.Lock() 331 watchers := make([]func(context.Context, []protocol.FileEvent), len(w.watchers)) 332 copy(watchers, w.watchers) 333 w.watcherMu.Unlock() 334 for _, w := range watchers { 335 w(ctx, evts) 336 } 337 return nil 338 } 339 340 // pollFiles updates w.files and calculates FileEvents corresponding to file 341 // state changes since the last poll. It does not call sendEvents. 342 func (w *Workdir) pollFiles() ([]protocol.FileEvent, error) { 343 w.fileMu.Lock() 344 defer w.fileMu.Unlock() 345 346 newFiles := make(map[string]fileID) 347 var evts []protocol.FileEvent 348 if err := filepath.Walk(string(w.RelativeTo), func(fp string, info os.FileInfo, err error) error { 349 if err != nil { 350 return err 351 } 352 // Skip directories and symbolic links (which may be links to directories). 353 // 354 // The latter matters for repos like Kubernetes, which use symlinks. 355 if info.Mode()&(fs.ModeDir|fs.ModeSymlink) != 0 { 356 return nil 357 } 358 359 // Opt: avoid reading the file if mtime is sufficiently old to be reliable. 360 // 361 // If mtime is recent, it may not sufficiently identify the file contents: 362 // a subsequent write could result in the same mtime. For these cases, we 363 // must read the file contents. 364 id := fileID{mtime: info.ModTime()} 365 if time.Since(info.ModTime()) < 2*time.Second { 366 data, err := os.ReadFile(fp) 367 if err != nil { 368 return err 369 } 370 id.hash = hashFile(data) 371 } 372 path := w.RelPath(fp) 373 newFiles[path] = id 374 375 if w.files != nil { 376 oldID, ok := w.files[path] 377 delete(w.files, path) 378 switch { 379 case !ok: 380 evts = append(evts, protocol.FileEvent{ 381 URI: w.URI(path), 382 Type: protocol.Created, 383 }) 384 case oldID != id: 385 changed := true 386 387 // Check whether oldID and id do not match because oldID was polled at 388 // a recent enough to time such as to require hashing. 389 // 390 // In this case, read the content to check whether the file actually 391 // changed. 392 if oldID.mtime.Equal(id.mtime) && oldID.hash != "" && id.hash == "" { 393 data, err := os.ReadFile(fp) 394 if err != nil { 395 return err 396 } 397 if hashFile(data) == oldID.hash { 398 changed = false 399 } 400 } 401 if changed { 402 evts = append(evts, protocol.FileEvent{ 403 URI: w.URI(path), 404 Type: protocol.Changed, 405 }) 406 } 407 } 408 } 409 410 return nil 411 }); err != nil { 412 return nil, err 413 } 414 415 // Any remaining files must have been deleted. 416 for path := range w.files { 417 evts = append(evts, protocol.FileEvent{ 418 URI: w.URI(path), 419 Type: protocol.Deleted, 420 }) 421 } 422 w.files = newFiles 423 return evts, nil 424 }