whatcanGOwrong
This commit is contained in:
@@ -0,0 +1,117 @@
|
||||
// Copyright 2022 The Go Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
//go:build ignore
|
||||
// +build ignore
|
||||
|
||||
// The copyfiles script copies the contents of the internal cmd/go robustio
|
||||
// package to the current directory, with adjustments to make it build.
|
||||
//
|
||||
// NOTE: In retrospect this script got out of hand, as we have to perform
|
||||
// various operations on the package to get it to build at old Go versions. If
|
||||
// in the future it proves to be flaky, delete it and just copy code manually.
|
||||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"go/build/constraint"
|
||||
"go/scanner"
|
||||
"go/token"
|
||||
"log"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"strings"
|
||||
)
|
||||
|
||||
func main() {
|
||||
dir := filepath.Join(runtime.GOROOT(), "src", "cmd", "go", "internal", "robustio")
|
||||
|
||||
entries, err := os.ReadDir(dir)
|
||||
if err != nil {
|
||||
log.Fatalf("reading the robustio dir: %v", err)
|
||||
}
|
||||
|
||||
// Collect file content so that we can validate before copying.
|
||||
fileContent := make(map[string][]byte)
|
||||
windowsImport := []byte("\t\"internal/syscall/windows\"\n")
|
||||
foundWindowsImport := false
|
||||
for _, entry := range entries {
|
||||
if strings.HasSuffix(entry.Name(), ".go") {
|
||||
pth := filepath.Join(dir, entry.Name())
|
||||
content, err := os.ReadFile(pth)
|
||||
if err != nil {
|
||||
log.Fatalf("reading %q: %v", entry.Name(), err)
|
||||
}
|
||||
|
||||
// Replace the use of internal/syscall/windows.ERROR_SHARING_VIOLATION
|
||||
// with a local constant.
|
||||
if entry.Name() == "robustio_windows.go" && bytes.Contains(content, windowsImport) {
|
||||
foundWindowsImport = true
|
||||
content = bytes.Replace(content, windowsImport, nil, 1)
|
||||
content = bytes.Replace(content, []byte("windows.ERROR_SHARING_VIOLATION"), []byte("ERROR_SHARING_VIOLATION"), -1)
|
||||
}
|
||||
|
||||
// Replace os.ReadFile with os.ReadFile (for 1.15 and older). We
|
||||
// attempt to match calls (via the '('), to avoid matching mentions of
|
||||
// os.ReadFile in comments.
|
||||
//
|
||||
// TODO(rfindley): once we (shortly!) no longer support 1.15, remove
|
||||
// this and break the build.
|
||||
if bytes.Contains(content, []byte("os.ReadFile(")) {
|
||||
content = bytes.Replace(content, []byte("\"os\""), []byte("\"io/ioutil\"\n\t\"os\""), 1)
|
||||
content = bytes.Replace(content, []byte("os.ReadFile("), []byte("os.ReadFile("), -1)
|
||||
}
|
||||
|
||||
// Add +build constraints, for 1.16.
|
||||
content = addPlusBuildConstraints(content)
|
||||
|
||||
fileContent[entry.Name()] = content
|
||||
}
|
||||
}
|
||||
|
||||
if !foundWindowsImport {
|
||||
log.Fatal("missing expected import of internal/syscall/windows in robustio_windows.go")
|
||||
}
|
||||
|
||||
for name, content := range fileContent {
|
||||
if err := os.WriteFile(name, content, 0644); err != nil {
|
||||
log.Fatalf("writing %q: %v", name, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// addPlusBuildConstraints splices in +build constraints for go:build
|
||||
// constraints encountered in the source.
|
||||
//
|
||||
// Gopls still builds at Go 1.16, which requires +build constraints.
|
||||
func addPlusBuildConstraints(src []byte) []byte {
|
||||
var s scanner.Scanner
|
||||
fset := token.NewFileSet()
|
||||
file := fset.AddFile("", fset.Base(), len(src))
|
||||
s.Init(file, src, nil /* no error handler */, scanner.ScanComments)
|
||||
|
||||
result := make([]byte, 0, len(src))
|
||||
lastInsertion := 0
|
||||
for {
|
||||
pos, tok, lit := s.Scan()
|
||||
if tok == token.EOF {
|
||||
break
|
||||
}
|
||||
if tok == token.COMMENT {
|
||||
if c, err := constraint.Parse(lit); err == nil {
|
||||
plusBuild, err := constraint.PlusBuildLines(c)
|
||||
if err != nil {
|
||||
log.Fatalf("computing +build constraint for %q: %v", lit, err)
|
||||
}
|
||||
insertAt := file.Offset(pos) + len(lit)
|
||||
result = append(result, src[lastInsertion:insertAt]...)
|
||||
result = append(result, []byte("\n"+strings.Join(plusBuild, "\n"))...)
|
||||
lastInsertion = insertAt
|
||||
}
|
||||
}
|
||||
}
|
||||
result = append(result, src[lastInsertion:]...)
|
||||
return result
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
// Copyright 2022 The Go Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package robustio
|
||||
|
||||
import "syscall"
|
||||
|
||||
// The robustio package is copied from cmd/go/internal/robustio, a package used
|
||||
// by the go command to retry known flaky operations on certain operating systems.
|
||||
|
||||
//go:generate go run copyfiles.go
|
||||
|
||||
// Since the gopls module cannot access internal/syscall/windows, copy a
|
||||
// necessary constant.
|
||||
const ERROR_SHARING_VIOLATION syscall.Errno = 32
|
||||
@@ -0,0 +1,69 @@
|
||||
// Copyright 2019 The Go Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
// Package robustio wraps I/O functions that are prone to failure on Windows,
|
||||
// transparently retrying errors up to an arbitrary timeout.
|
||||
//
|
||||
// Errors are classified heuristically and retries are bounded, so the functions
|
||||
// in this package do not completely eliminate spurious errors. However, they do
|
||||
// significantly reduce the rate of failure in practice.
|
||||
//
|
||||
// If so, the error will likely wrap one of:
|
||||
// The functions in this package do not completely eliminate spurious errors,
|
||||
// but substantially reduce their rate of occurrence in practice.
|
||||
package robustio
|
||||
|
||||
import "time"
|
||||
|
||||
// Rename is like os.Rename, but on Windows retries errors that may occur if the
|
||||
// file is concurrently read or overwritten.
|
||||
//
|
||||
// (See golang.org/issue/31247 and golang.org/issue/32188.)
|
||||
func Rename(oldpath, newpath string) error {
|
||||
return rename(oldpath, newpath)
|
||||
}
|
||||
|
||||
// ReadFile is like os.ReadFile, but on Windows retries errors that may
|
||||
// occur if the file is concurrently replaced.
|
||||
//
|
||||
// (See golang.org/issue/31247 and golang.org/issue/32188.)
|
||||
func ReadFile(filename string) ([]byte, error) {
|
||||
return readFile(filename)
|
||||
}
|
||||
|
||||
// RemoveAll is like os.RemoveAll, but on Windows retries errors that may occur
|
||||
// if an executable file in the directory has recently been executed.
|
||||
//
|
||||
// (See golang.org/issue/19491.)
|
||||
func RemoveAll(path string) error {
|
||||
return removeAll(path)
|
||||
}
|
||||
|
||||
// IsEphemeralError reports whether err is one of the errors that the functions
|
||||
// in this package attempt to mitigate.
|
||||
//
|
||||
// Errors considered ephemeral include:
|
||||
// - syscall.ERROR_ACCESS_DENIED
|
||||
// - syscall.ERROR_FILE_NOT_FOUND
|
||||
// - internal/syscall/windows.ERROR_SHARING_VIOLATION
|
||||
//
|
||||
// This set may be expanded in the future; programs must not rely on the
|
||||
// non-ephemerality of any given error.
|
||||
func IsEphemeralError(err error) bool {
|
||||
return isEphemeralError(err)
|
||||
}
|
||||
|
||||
// A FileID uniquely identifies a file in the file system.
|
||||
//
|
||||
// If GetFileID(name1) returns the same ID as GetFileID(name2), the two file
|
||||
// names denote the same file.
|
||||
// A FileID is comparable, and thus suitable for use as a map key.
|
||||
type FileID struct {
|
||||
device, inode uint64
|
||||
}
|
||||
|
||||
// GetFileID returns the file system's identifier for the file, and its
|
||||
// modification time.
|
||||
// Like os.Stat, it reads through symbolic links.
|
||||
func GetFileID(filename string) (FileID, time.Time, error) { return getFileID(filename) }
|
||||
@@ -0,0 +1,21 @@
|
||||
// Copyright 2019 The Go Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package robustio
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"syscall"
|
||||
)
|
||||
|
||||
const errFileNotFound = syscall.ENOENT
|
||||
|
||||
// isEphemeralError returns true if err may be resolved by waiting.
|
||||
func isEphemeralError(err error) bool {
|
||||
var errno syscall.Errno
|
||||
if errors.As(err, &errno) {
|
||||
return errno == errFileNotFound
|
||||
}
|
||||
return false
|
||||
}
|
||||
@@ -0,0 +1,92 @@
|
||||
// Copyright 2019 The Go Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
//go:build windows || darwin
|
||||
// +build windows darwin
|
||||
|
||||
package robustio
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"math/rand"
|
||||
"os"
|
||||
"syscall"
|
||||
"time"
|
||||
)
|
||||
|
||||
const arbitraryTimeout = 2000 * time.Millisecond
|
||||
|
||||
// retry retries ephemeral errors from f up to an arbitrary timeout
|
||||
// to work around filesystem flakiness on Windows and Darwin.
|
||||
func retry(f func() (err error, mayRetry bool)) error {
|
||||
var (
|
||||
bestErr error
|
||||
lowestErrno syscall.Errno
|
||||
start time.Time
|
||||
nextSleep time.Duration = 1 * time.Millisecond
|
||||
)
|
||||
for {
|
||||
err, mayRetry := f()
|
||||
if err == nil || !mayRetry {
|
||||
return err
|
||||
}
|
||||
|
||||
var errno syscall.Errno
|
||||
if errors.As(err, &errno) && (lowestErrno == 0 || errno < lowestErrno) {
|
||||
bestErr = err
|
||||
lowestErrno = errno
|
||||
} else if bestErr == nil {
|
||||
bestErr = err
|
||||
}
|
||||
|
||||
if start.IsZero() {
|
||||
start = time.Now()
|
||||
} else if d := time.Since(start) + nextSleep; d >= arbitraryTimeout {
|
||||
break
|
||||
}
|
||||
time.Sleep(nextSleep)
|
||||
nextSleep += time.Duration(rand.Int63n(int64(nextSleep)))
|
||||
}
|
||||
|
||||
return bestErr
|
||||
}
|
||||
|
||||
// rename is like os.Rename, but retries ephemeral errors.
|
||||
//
|
||||
// On Windows it wraps os.Rename, which (as of 2019-06-04) uses MoveFileEx with
|
||||
// MOVEFILE_REPLACE_EXISTING.
|
||||
//
|
||||
// Windows also provides a different system call, ReplaceFile,
|
||||
// that provides similar semantics, but perhaps preserves more metadata. (The
|
||||
// documentation on the differences between the two is very sparse.)
|
||||
//
|
||||
// Empirical error rates with MoveFileEx are lower under modest concurrency, so
|
||||
// for now we're sticking with what the os package already provides.
|
||||
func rename(oldpath, newpath string) (err error) {
|
||||
return retry(func() (err error, mayRetry bool) {
|
||||
err = os.Rename(oldpath, newpath)
|
||||
return err, isEphemeralError(err)
|
||||
})
|
||||
}
|
||||
|
||||
// readFile is like os.ReadFile, but retries ephemeral errors.
|
||||
func readFile(filename string) ([]byte, error) {
|
||||
var b []byte
|
||||
err := retry(func() (err error, mayRetry bool) {
|
||||
b, err = os.ReadFile(filename)
|
||||
|
||||
// Unlike in rename, we do not retry errFileNotFound here: it can occur
|
||||
// as a spurious error, but the file may also genuinely not exist, so the
|
||||
// increase in robustness is probably not worth the extra latency.
|
||||
return err, isEphemeralError(err) && !errors.Is(err, errFileNotFound)
|
||||
})
|
||||
return b, err
|
||||
}
|
||||
|
||||
func removeAll(path string) error {
|
||||
return retry(func() (err error, mayRetry bool) {
|
||||
err = os.RemoveAll(path)
|
||||
return err, isEphemeralError(err)
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
// Copyright 2019 The Go Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
//go:build !windows && !darwin
|
||||
// +build !windows,!darwin
|
||||
|
||||
package robustio
|
||||
|
||||
import (
|
||||
"os"
|
||||
)
|
||||
|
||||
func rename(oldpath, newpath string) error {
|
||||
return os.Rename(oldpath, newpath)
|
||||
}
|
||||
|
||||
func readFile(filename string) ([]byte, error) {
|
||||
return os.ReadFile(filename)
|
||||
}
|
||||
|
||||
func removeAll(path string) error {
|
||||
return os.RemoveAll(path)
|
||||
}
|
||||
|
||||
func isEphemeralError(err error) bool {
|
||||
return false
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
// Copyright 2022 The Go Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
//go:build plan9
|
||||
// +build plan9
|
||||
|
||||
package robustio
|
||||
|
||||
import (
|
||||
"os"
|
||||
"syscall"
|
||||
"time"
|
||||
)
|
||||
|
||||
func getFileID(filename string) (FileID, time.Time, error) {
|
||||
fi, err := os.Stat(filename)
|
||||
if err != nil {
|
||||
return FileID{}, time.Time{}, err
|
||||
}
|
||||
dir := fi.Sys().(*syscall.Dir)
|
||||
return FileID{
|
||||
device: uint64(dir.Type)<<32 | uint64(dir.Dev),
|
||||
inode: dir.Qid.Path,
|
||||
}, fi.ModTime(), nil
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
// Copyright 2022 The Go Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
//go:build !windows && !plan9
|
||||
// +build !windows,!plan9
|
||||
|
||||
// TODO(adonovan): use 'unix' tag when go1.19 can be assumed.
|
||||
|
||||
package robustio
|
||||
|
||||
import (
|
||||
"os"
|
||||
"syscall"
|
||||
"time"
|
||||
)
|
||||
|
||||
func getFileID(filename string) (FileID, time.Time, error) {
|
||||
fi, err := os.Stat(filename)
|
||||
if err != nil {
|
||||
return FileID{}, time.Time{}, err
|
||||
}
|
||||
stat := fi.Sys().(*syscall.Stat_t)
|
||||
return FileID{
|
||||
device: uint64(stat.Dev), // (int32 on darwin, uint64 on linux)
|
||||
inode: stat.Ino,
|
||||
}, fi.ModTime(), nil
|
||||
}
|
||||
@@ -0,0 +1,101 @@
|
||||
// Copyright 2022 The Go Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package robustio_test
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"golang.org/x/tools/internal/robustio"
|
||||
)
|
||||
|
||||
func checkOSLink(t *testing.T, err error) {
|
||||
if err == nil {
|
||||
return
|
||||
}
|
||||
|
||||
t.Helper()
|
||||
switch runtime.GOOS {
|
||||
case "aix", "darwin", "dragonfly", "freebsd", "illumos", "linux", "netbsd", "openbsd", "solaris":
|
||||
// Non-mobile OS known to always support os.Symlink and os.Link.
|
||||
t.Fatal(err)
|
||||
default:
|
||||
t.Skipf("skipping due to error on %v: %v", runtime.GOOS, err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestFileInfo(t *testing.T) {
|
||||
// A nonexistent file has no ID.
|
||||
nonexistent := filepath.Join(t.TempDir(), "nonexistent")
|
||||
if _, _, err := robustio.GetFileID(nonexistent); err == nil {
|
||||
t.Fatalf("GetFileID(nonexistent) succeeded unexpectedly")
|
||||
}
|
||||
|
||||
// A regular file has an ID.
|
||||
real := filepath.Join(t.TempDir(), "real")
|
||||
if err := os.WriteFile(real, nil, 0644); err != nil {
|
||||
t.Fatalf("can't create regular file: %v", err)
|
||||
}
|
||||
realID, realMtime, err := robustio.GetFileID(real)
|
||||
if err != nil {
|
||||
t.Fatalf("can't get ID of regular file: %v", err)
|
||||
}
|
||||
|
||||
// Sleep so that we get a new mtime for subsequent writes.
|
||||
time.Sleep(2 * time.Second)
|
||||
|
||||
// A second regular file has a different ID.
|
||||
real2 := filepath.Join(t.TempDir(), "real2")
|
||||
if err := os.WriteFile(real2, nil, 0644); err != nil {
|
||||
t.Fatalf("can't create second regular file: %v", err)
|
||||
}
|
||||
real2ID, real2Mtime, err := robustio.GetFileID(real2)
|
||||
if err != nil {
|
||||
t.Fatalf("can't get ID of second regular file: %v", err)
|
||||
}
|
||||
if realID == real2ID {
|
||||
t.Errorf("realID %+v == real2ID %+v", realID, real2ID)
|
||||
}
|
||||
if realMtime.Equal(real2Mtime) {
|
||||
t.Errorf("realMtime %v == real2Mtime %v", realMtime, real2Mtime)
|
||||
}
|
||||
|
||||
// A symbolic link has the same ID as its target.
|
||||
t.Run("symlink", func(t *testing.T) {
|
||||
symlink := filepath.Join(t.TempDir(), "symlink")
|
||||
checkOSLink(t, os.Symlink(real, symlink))
|
||||
|
||||
symlinkID, symlinkMtime, err := robustio.GetFileID(symlink)
|
||||
if err != nil {
|
||||
t.Fatalf("can't get ID of symbolic link: %v", err)
|
||||
}
|
||||
if realID != symlinkID {
|
||||
t.Errorf("realID %+v != symlinkID %+v", realID, symlinkID)
|
||||
}
|
||||
if !realMtime.Equal(symlinkMtime) {
|
||||
t.Errorf("realMtime %v != symlinkMtime %v", realMtime, symlinkMtime)
|
||||
}
|
||||
})
|
||||
|
||||
// Two hard-linked files have the same ID.
|
||||
t.Run("hardlink", func(t *testing.T) {
|
||||
hardlink := filepath.Join(t.TempDir(), "hardlink")
|
||||
checkOSLink(t, os.Link(real, hardlink))
|
||||
|
||||
hardlinkID, hardlinkMtime, err := robustio.GetFileID(hardlink)
|
||||
if err != nil {
|
||||
t.Fatalf("can't get ID of hard link: %v", err)
|
||||
}
|
||||
if realID != hardlinkID {
|
||||
t.Errorf("realID %+v != hardlinkID %+v", realID, hardlinkID)
|
||||
}
|
||||
if !realMtime.Equal(hardlinkMtime) {
|
||||
t.Errorf("realMtime %v != hardlinkMtime %v", realMtime, hardlinkMtime)
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
// Copyright 2019 The Go Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package robustio
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"syscall"
|
||||
"time"
|
||||
)
|
||||
|
||||
const errFileNotFound = syscall.ERROR_FILE_NOT_FOUND
|
||||
|
||||
// isEphemeralError returns true if err may be resolved by waiting.
|
||||
func isEphemeralError(err error) bool {
|
||||
var errno syscall.Errno
|
||||
if errors.As(err, &errno) {
|
||||
switch errno {
|
||||
case syscall.ERROR_ACCESS_DENIED,
|
||||
syscall.ERROR_FILE_NOT_FOUND,
|
||||
ERROR_SHARING_VIOLATION:
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// Note: it may be convenient to have this helper return fs.FileInfo, but
|
||||
// implementing this is actually quite involved on Windows. Since we only
|
||||
// currently use mtime, keep it simple.
|
||||
func getFileID(filename string) (FileID, time.Time, error) {
|
||||
filename16, err := syscall.UTF16PtrFromString(filename)
|
||||
if err != nil {
|
||||
return FileID{}, time.Time{}, err
|
||||
}
|
||||
h, err := syscall.CreateFile(filename16, 0, 0, nil, syscall.OPEN_EXISTING, uint32(syscall.FILE_FLAG_BACKUP_SEMANTICS), 0)
|
||||
if err != nil {
|
||||
return FileID{}, time.Time{}, err
|
||||
}
|
||||
defer syscall.CloseHandle(h)
|
||||
var i syscall.ByHandleFileInformation
|
||||
if err := syscall.GetFileInformationByHandle(h, &i); err != nil {
|
||||
return FileID{}, time.Time{}, err
|
||||
}
|
||||
mtime := time.Unix(0, i.LastWriteTime.Nanoseconds())
|
||||
return FileID{
|
||||
device: uint64(i.VolumeSerialNumber),
|
||||
inode: uint64(i.FileIndexHigh)<<32 | uint64(i.FileIndexLow),
|
||||
}, mtime, nil
|
||||
}
|
||||
Reference in New Issue
Block a user