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  }