gopls/internal/lsp/fake: in (*Workdir).RenameFile, fall back to read + write
os.Rename cannot portably rename files across directories; notably,
that operation fails with "invalid argument" on some Plan 9
filesystems.
Fixes golang/go#57111.
Change-Id: Ifd108bb58fa968fc2c8a21b99211a904d6d3d4e6
Reviewed-on: https://go-review.googlesource.com/c/tools/+/455515
Run-TryBot: Bryan Mills <bcmills@google.com>
Reviewed-by: Robert Findley <rfindley@google.com>
gopls-CI: kokoro <noreply+kokoro@google.com>
TryBot-Result: Gopher Robot <gobot@golang.org>
Auto-Submit: Bryan Mills <bcmills@google.com>
diff --git a/gopls/internal/lsp/fake/workdir.go b/gopls/internal/lsp/fake/workdir.go
index 2b426e4..4c8aa12 100644
--- a/gopls/internal/lsp/fake/workdir.go
+++ b/gopls/internal/lsp/fake/workdir.go
@@ -330,12 +330,87 @@
// RenameFile performs an on disk-renaming of the workdir-relative oldPath to
// workdir-relative newPath.
+//
+// oldPath must either be a regular file or in the same directory as newPath.
func (w *Workdir) RenameFile(ctx context.Context, oldPath, newPath string) error {
oldAbs := w.AbsPath(oldPath)
newAbs := w.AbsPath(newPath)
- if err := robustio.Rename(oldAbs, newAbs); err != nil {
- return err
+ w.fileMu.Lock()
+ defer w.fileMu.Unlock()
+
+ // For os.Rename, “OS-specific restrictions may apply when oldpath and newpath
+ // are in different directories.” If that applies here, we may fall back to
+ // ReadFile, WriteFile, and RemoveFile to perform the rename non-atomically.
+ //
+ // However, the fallback path only works for regular files: renaming a
+ // directory would be much more complex and isn't needed for our tests.
+ fallbackOk := false
+ if filepath.Dir(oldAbs) != filepath.Dir(newAbs) {
+ fi, err := os.Stat(oldAbs)
+ if err == nil && !fi.Mode().IsRegular() {
+ return &os.PathError{
+ Op: "RenameFile",
+ Path: oldPath,
+ Err: fmt.Errorf("%w: file is not regular and not in the same directory as %s", os.ErrInvalid, newPath),
+ }
+ }
+ fallbackOk = true
+ }
+
+ var renameErr error
+ const debugFallback = false
+ if fallbackOk && debugFallback {
+ renameErr = fmt.Errorf("%w: debugging fallback path", os.ErrInvalid)
+ } else {
+ renameErr = robustio.Rename(oldAbs, newAbs)
+ }
+ if renameErr != nil {
+ if !fallbackOk {
+ return renameErr // The OS-specific Rename restrictions do not apply.
+ }
+
+ content, err := w.ReadFile(oldPath)
+ if err != nil {
+ // If we can't even read the file, the error from Rename may be accurate.
+ return renameErr
+ }
+ fi, err := os.Stat(newAbs)
+ if err == nil {
+ if fi.IsDir() {
+ // “If newpath already exists and is not a directory, Rename replaces it.”
+ // But if it is a directory, maybe not?
+ return renameErr
+ }
+ // On most platforms, Rename replaces the named file with a new file,
+ // rather than overwriting the existing file it in place. Mimic that
+ // behavior here.
+ if err := robustio.RemoveAll(newAbs); err != nil {
+ // Maybe we don't have permission to replace newPath?
+ return renameErr
+ }
+ } else if !os.IsNotExist(err) {
+ // If the destination path already exists or there is some problem with it,
+ // the error from Rename may be accurate.
+ return renameErr
+ }
+ if writeErr := WriteFileData(newPath, []byte(content), w.RelativeTo); writeErr != nil {
+ // At this point we have tried to actually write the file.
+ // If it still doesn't exist, assume that the error from Rename was accurate:
+ // for example, maybe we don't have permission to create the new path.
+ // Otherwise, return the error from the write, which may indicate some
+ // other problem (such as a full disk).
+ if _, statErr := os.Stat(newAbs); !os.IsNotExist(statErr) {
+ return writeErr
+ }
+ return renameErr
+ }
+ if err := robustio.RemoveAll(oldAbs); err != nil {
+ // If we failed to remove the old file, that may explain the Rename error too.
+ // Make a best effort to back out the write to the new path.
+ robustio.RemoveAll(newAbs)
+ return renameErr
+ }
}
// Send synthetic file events for the renaming. Renamed files are handled as
@@ -346,7 +421,7 @@
w.fileEvent(newPath, protocol.Created),
}
w.sendEvents(ctx, events)
-
+ delete(w.files, oldPath)
return nil
}