locks_unix.go 4.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148
  1. //go:build darwin || dragonfly || freebsd || illumos || linux || netbsd || openbsd
  2. // The above platforms support flock()
  3. package fileutil
  4. import (
  5. "bytes"
  6. "fmt"
  7. "io"
  8. "os"
  9. "syscall"
  10. "github.com/opencost/opencost/core/pkg/log"
  11. )
  12. // LockFile directly attempts to flock EX the file instance provided.
  13. func LockFile(f *os.File) error {
  14. if err := syscall.Flock(int(f.Fd()), syscall.LOCK_EX); err != nil {
  15. return fmt.Errorf("unexpected error flock()-ing with EX: %w", err)
  16. }
  17. return nil
  18. }
  19. // ReadLockFile directly attempts to flock SH the file instance provided.
  20. func ReadLockFile(f *os.File) error {
  21. if err := syscall.Flock(int(f.Fd()), syscall.LOCK_SH); err != nil {
  22. return fmt.Errorf("unexpected error flock()-ing FD: %d directly: %w", f.Fd(), err)
  23. }
  24. return nil
  25. }
  26. // UnlockFile directly attempts to flock UN the file instance provided.
  27. func UnlockFile(f *os.File) error {
  28. if err := syscall.Flock(int(f.Fd()), syscall.LOCK_UN); err != nil {
  29. return fmt.Errorf("unexpected error flock()-ing FD %d with UN: %w", f.Fd(), err)
  30. }
  31. return nil
  32. }
  33. // WriteLockedFD uses the flock() syscall to safely write to an open file as
  34. // long as other users of the file are also using flock()-based access.
  35. //
  36. // WriteLocked will block until it gets lock access.
  37. //
  38. // The file will be truncated before writing and at the end of writing the
  39. // FD will be reset to position 0.
  40. //
  41. // For the reasons outlined best in https://lwn.net/Articles/586904/ this uses
  42. // flock() instead of fcntl(). The ability to lock byte ranges is not necessary
  43. // and flock() has better behavior.
  44. func WriteLockedFD(f *os.File, data []byte) (int, error) {
  45. // For the reasons outlined best in https://lwn.net/Articles/586904/ we're
  46. // going to use flock() instead of fcntl() because we want a whole-file lock.
  47. if err := syscall.Flock(int(f.Fd()), syscall.LOCK_EX); err != nil {
  48. return 0, fmt.Errorf("unexpected error flock()-ing with EX: %w", err)
  49. }
  50. defer func() {
  51. if err := syscall.Flock(int(f.Fd()), syscall.LOCK_UN); err != nil {
  52. log.Errorf("unexpected error flock()-ing FD %d with UN after writing: %s", f.Fd(), err)
  53. }
  54. }()
  55. if err := f.Truncate(0); err != nil {
  56. return 0, fmt.Errorf("truncating: %w", err)
  57. }
  58. if _, err := f.Seek(0, 0); err != nil {
  59. return 0, fmt.Errorf("seeking to 0 before write: %w", err)
  60. }
  61. defer func() {
  62. if _, err := f.Seek(0, 0); err != nil {
  63. log.Errorf("unexpected error seeking to 0 after write on FD %d: %s", f.Fd(), err)
  64. }
  65. }()
  66. n, err := f.Write(data)
  67. if err != nil {
  68. return n, fmt.Errorf("writing data: %w", err)
  69. }
  70. return n, nil
  71. }
  72. // WriteLocked opens the file and then calls WriteLockedFD.
  73. func WriteLocked(filename string, data []byte) (int, error) {
  74. file, err := os.OpenFile(filename, os.O_RDWR|os.O_CREATE, 0600)
  75. if err != nil {
  76. return 0, fmt.Errorf("opening %s: %w", filename, err)
  77. }
  78. defer file.Close()
  79. return WriteLockedFD(file, data)
  80. }
  81. // ReadLockedFD uses the flock() syscall to safely read from an open file as
  82. // long as other users of the file are also using flock()-based access.
  83. //
  84. // ReadLockedFD will block until it gets lock access.
  85. //
  86. // This will read the file in full, from 0, regardless of the current
  87. // position and then reset the position to 0.
  88. //
  89. // For the reasons outlined best in https://lwn.net/Articles/586904/ this uses
  90. // flock() instead of fcntl(). The ability to lock byte ranges is not necessary
  91. // and flock() has better behavior.
  92. func ReadLockedFD(f *os.File) ([]byte, error) {
  93. // For the reasons outlined best in https://lwn.net/Articles/586904/ we're
  94. // going to use flock() instead of fcntl() because we want a whole-file lock.
  95. if err := syscall.Flock(int(f.Fd()), syscall.LOCK_SH); err != nil {
  96. return nil, fmt.Errorf("unexpected error flock()-ing with SH: %w", err)
  97. }
  98. defer func() {
  99. if err := syscall.Flock(int(f.Fd()), syscall.LOCK_UN); err != nil {
  100. log.Errorf("unexpected error flock()-ing FD %d with UN reading: %s", f.Fd(), err)
  101. }
  102. }()
  103. if _, err := f.Seek(0, 0); err != nil {
  104. return nil, fmt.Errorf("seeking to 0 before read: %w", err)
  105. }
  106. defer func() {
  107. if _, err := f.Seek(0, 0); err != nil {
  108. log.Errorf("unexpected error seeking to 0 after read on FD %d: %s", f.Fd(), err)
  109. }
  110. }()
  111. buf := bytes.NewBuffer(nil)
  112. if _, err := io.Copy(buf, f); err != nil {
  113. if err := syscall.Flock(int(f.Fd()), syscall.LOCK_UN); err != nil {
  114. log.Errorf("unexpected error flock()-ing with UN after error reading: %s", err)
  115. }
  116. return nil, fmt.Errorf("copying data out of file: %w", err)
  117. }
  118. return buf.Bytes(), nil
  119. }
  120. // ReadLocked opens the given file and then calls ReadLockedFD.
  121. func ReadLocked(filename string) ([]byte, error) {
  122. file, err := os.OpenFile(filename, os.O_RDONLY, 0600)
  123. if err != nil {
  124. return nil, fmt.Errorf("opening %s: %w", filename, err)
  125. }
  126. defer file.Close()
  127. return ReadLockedFD(file)
  128. }