locks_unix.go 3.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124
  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. // WriteLockedFD uses the flock() syscall to safely write to an open file as
  13. // long as other users of the file are also using flock()-based access.
  14. //
  15. // WriteLocked will block until it gets lock access.
  16. //
  17. // The file will be truncated before writing and at the end of writing the
  18. // FD will be reset to position 0.
  19. //
  20. // For the reasons outlined best in https://lwn.net/Articles/586904/ this uses
  21. // flock() instead of fcntl(). The ability to lock byte ranges is not necessary
  22. // and flock() has better behavior.
  23. func WriteLockedFD(f *os.File, data []byte) (int, error) {
  24. // For the reasons outlined best in https://lwn.net/Articles/586904/ we're
  25. // going to use flock() instead of fcntl() because we want a whole-file lock.
  26. if err := syscall.Flock(int(f.Fd()), syscall.LOCK_EX); err != nil {
  27. return 0, fmt.Errorf("unexpected error flock()-ing with EX: %w", err)
  28. }
  29. defer func() {
  30. if err := syscall.Flock(int(f.Fd()), syscall.LOCK_UN); err != nil {
  31. log.Errorf("unexpected error flock()-ing FD %d with UN after writing: %s", f.Fd(), err)
  32. }
  33. }()
  34. if err := f.Truncate(0); err != nil {
  35. return 0, fmt.Errorf("truncating: %w", err)
  36. }
  37. if _, err := f.Seek(0, 0); err != nil {
  38. return 0, fmt.Errorf("seeking to 0 before write: %w", err)
  39. }
  40. defer func() {
  41. if _, err := f.Seek(0, 0); err != nil {
  42. log.Errorf("unexpected error seeking to 0 after write on FD %d: %s", f.Fd(), err)
  43. }
  44. }()
  45. n, err := f.Write(data)
  46. if err != nil {
  47. return n, fmt.Errorf("writing data: %w", err)
  48. }
  49. return n, nil
  50. }
  51. // WriteLocked opens the file and then calls WriteLockedFD.
  52. func WriteLocked(filename string, data []byte) (int, error) {
  53. file, err := os.OpenFile(filename, os.O_RDWR|os.O_CREATE, 0600)
  54. if err != nil {
  55. return 0, fmt.Errorf("opening %s: %w", filename, err)
  56. }
  57. defer file.Close()
  58. return WriteLockedFD(file, data)
  59. }
  60. // ReadLockedFD uses the flock() syscall to safely read from an open file as
  61. // long as other users of the file are also using flock()-based access.
  62. //
  63. // ReadLockedFD will block until it gets lock access.
  64. //
  65. // This will read the file in full, from 0, regardless of the current
  66. // position and then reset the position to 0.
  67. //
  68. // For the reasons outlined best in https://lwn.net/Articles/586904/ this uses
  69. // flock() instead of fcntl(). The ability to lock byte ranges is not necessary
  70. // and flock() has better behavior.
  71. func ReadLockedFD(f *os.File) ([]byte, error) {
  72. // For the reasons outlined best in https://lwn.net/Articles/586904/ we're
  73. // going to use flock() instead of fcntl() because we want a whole-file lock.
  74. if err := syscall.Flock(int(f.Fd()), syscall.LOCK_SH); err != nil {
  75. return nil, fmt.Errorf("unexpected error flock()-ing with SH: %w", err)
  76. }
  77. defer func() {
  78. if err := syscall.Flock(int(f.Fd()), syscall.LOCK_UN); err != nil {
  79. log.Errorf("unexpected error flock()-ing FD %d with UN reading: %s", f.Fd(), err)
  80. }
  81. }()
  82. if _, err := f.Seek(0, 0); err != nil {
  83. return nil, fmt.Errorf("seeking to 0 before read: %w", err)
  84. }
  85. defer func() {
  86. if _, err := f.Seek(0, 0); err != nil {
  87. log.Errorf("unexpected error seeking to 0 after read on FD %d: %s", f.Fd(), err)
  88. }
  89. }()
  90. buf := bytes.NewBuffer(nil)
  91. if _, err := io.Copy(buf, f); err != nil {
  92. if err := syscall.Flock(int(f.Fd()), syscall.LOCK_UN); err != nil {
  93. log.Errorf("unexpected error flock()-ing with UN after error reading: %s", err)
  94. }
  95. return nil, fmt.Errorf("copying data out of file: %w", err)
  96. }
  97. return buf.Bytes(), nil
  98. }
  99. // ReadLocked opens the given file and then calls ReadLockedFD.
  100. func ReadLocked(filename string) ([]byte, error) {
  101. file, err := os.OpenFile(filename, os.O_RDONLY, 0600)
  102. if err != nil {
  103. return nil, fmt.Errorf("opening %s: %w", filename, err)
  104. }
  105. defer file.Close()
  106. return ReadLockedFD(file)
  107. }