You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

152 lines
4.9 KiB

  1. package common
  2. import (
  3. fmt "fmt"
  4. "io"
  5. "io/ioutil"
  6. "os"
  7. "path/filepath"
  8. "strconv"
  9. "strings"
  10. "sync"
  11. "time"
  12. )
  13. const (
  14. atomicWriteFilePrefix = "write-file-atomic-"
  15. // Maximum number of atomic write file conflicts before we start reseeding
  16. // (reduced from golang's default 10 due to using an increased randomness space)
  17. atomicWriteFileMaxNumConflicts = 5
  18. // Maximum number of attempts to make at writing the write file before giving up
  19. // (reduced from golang's default 10000 due to using an increased randomness space)
  20. atomicWriteFileMaxNumWriteAttempts = 1000
  21. // LCG constants from Donald Knuth MMIX
  22. // This LCG's has a period equal to 2**64
  23. lcgA = 6364136223846793005
  24. lcgC = 1442695040888963407
  25. // Create in case it doesn't exist and force kernel
  26. // flush, which still leaves the potential of lingering disk cache.
  27. // Never overwrites files
  28. atomicWriteFileFlag = os.O_WRONLY | os.O_CREATE | os.O_SYNC | os.O_TRUNC | os.O_EXCL
  29. )
  30. var (
  31. atomicWriteFileRand uint64
  32. atomicWriteFileRandMu sync.Mutex
  33. )
  34. func writeFileRandReseed() uint64 {
  35. // Scale the PID, to minimize the chance that two processes seeded at similar times
  36. // don't get the same seed. Note that PID typically ranges in [0, 2**15), but can be
  37. // up to 2**22 under certain configurations. We left bit-shift the PID by 20, so that
  38. // a PID difference of one corresponds to a time difference of 2048 seconds.
  39. // The important thing here is that now for a seed conflict, they would both have to be on
  40. // the correct nanosecond offset, and second-based offset, which is much less likely than
  41. // just a conflict with the correct nanosecond offset.
  42. return uint64(time.Now().UnixNano() + int64(os.Getpid()<<20))
  43. }
  44. // Use a fast thread safe LCG for atomic write file names.
  45. // Returns a string corresponding to a 64 bit int.
  46. // If it was a negative int, the leading number is a 0.
  47. func randWriteFileSuffix() string {
  48. atomicWriteFileRandMu.Lock()
  49. r := atomicWriteFileRand
  50. if r == 0 {
  51. r = writeFileRandReseed()
  52. }
  53. // Update randomness according to lcg
  54. r = r*lcgA + lcgC
  55. atomicWriteFileRand = r
  56. atomicWriteFileRandMu.Unlock()
  57. // Can have a negative name, replace this in the following
  58. suffix := strconv.Itoa(int(r))
  59. if string(suffix[0]) == "-" {
  60. // Replace first "-" with "0". This is purely for UI clarity,
  61. // as otherwhise there would be two `-` in a row.
  62. suffix = strings.Replace(suffix, "-", "0", 1)
  63. }
  64. return suffix
  65. }
  66. // WriteFileAtomic creates a temporary file with data and provided perm and
  67. // swaps it atomically with filename if successful.
  68. func WriteFileAtomic(filename string, data []byte, perm os.FileMode) (err error) {
  69. // This implementation is inspired by the golang stdlibs method of creating
  70. // tempfiles. Notable differences are that we use different flags, a 64 bit LCG
  71. // and handle negatives differently.
  72. // The core reason we can't use golang's TempFile is that we must write
  73. // to the file synchronously, as we need this to persist to disk.
  74. // We also open it in write-only mode, to avoid concerns that arise with read.
  75. var (
  76. dir = filepath.Dir(filename)
  77. f *os.File
  78. )
  79. nconflict := 0
  80. // Limit the number of attempts to create a file. Something is seriously
  81. // wrong if it didn't get created after 1000 attempts, and we don't want
  82. // an infinite loop
  83. i := 0
  84. for ; i < atomicWriteFileMaxNumWriteAttempts; i++ {
  85. name := filepath.Join(dir, atomicWriteFilePrefix+randWriteFileSuffix())
  86. f, err = os.OpenFile(name, atomicWriteFileFlag, perm)
  87. // If the file already exists, try a new file
  88. if os.IsExist(err) {
  89. // If the files exists too many times, start reseeding as we've
  90. // likely hit another instances seed.
  91. if nconflict++; nconflict > atomicWriteFileMaxNumConflicts {
  92. atomicWriteFileRandMu.Lock()
  93. atomicWriteFileRand = writeFileRandReseed()
  94. atomicWriteFileRandMu.Unlock()
  95. }
  96. continue
  97. } else if err != nil {
  98. return err
  99. }
  100. break
  101. }
  102. if i == atomicWriteFileMaxNumWriteAttempts {
  103. return fmt.Errorf("Could not create atomic write file after %d attempts", i)
  104. }
  105. // Clean up in any case. Defer stacking order is last-in-first-out.
  106. defer os.Remove(f.Name())
  107. defer f.Close()
  108. if n, err := f.Write(data); err != nil {
  109. return err
  110. } else if n < len(data) {
  111. return io.ErrShortWrite
  112. }
  113. // Close the file before renaming it, otherwise it will cause "The process
  114. // cannot access the file because it is being used by another process." on windows.
  115. f.Close()
  116. return os.Rename(f.Name(), filename)
  117. }
  118. //--------------------------------------------------------------------------------
  119. func Tempfile(prefix string) (*os.File, string) {
  120. file, err := ioutil.TempFile("", prefix)
  121. if err != nil {
  122. PanicCrisis(err)
  123. }
  124. return file, file.Name()
  125. }
  126. func Tempdir(prefix string) (*os.File, string) {
  127. tempDir := os.TempDir() + "/" + prefix + RandStr(12)
  128. err := EnsureDir(tempDir, 0700)
  129. if err != nil {
  130. panic(Fmt("Error creating temp dir: %v", err))
  131. }
  132. dir, err := os.Open(tempDir)
  133. if err != nil {
  134. panic(Fmt("Error opening temp dir: %v", err))
  135. }
  136. return dir, tempDir
  137. }