|
|
@@ -0,0 +1,124 @@
|
|
|
+//go:build darwin || dragonfly || freebsd || illumos || linux || netbsd || openbsd
|
|
|
+
|
|
|
+// The above platforms support flock()
|
|
|
+
|
|
|
+package fileutil
|
|
|
+
|
|
|
+import (
|
|
|
+ "bytes"
|
|
|
+ "fmt"
|
|
|
+ "io"
|
|
|
+ "os"
|
|
|
+ "syscall"
|
|
|
+
|
|
|
+ "github.com/opencost/opencost/pkg/log"
|
|
|
+)
|
|
|
+
|
|
|
+// WriteLockedFD uses the flock() syscall to safely write to an open file as
|
|
|
+// long as other users of the file are also using flock()-based access.
|
|
|
+//
|
|
|
+// WriteLocked will block until it gets lock access.
|
|
|
+//
|
|
|
+// The file will be truncated before writing and at the end of writing the
|
|
|
+// FD will be reset to position 0.
|
|
|
+//
|
|
|
+// For the reasons outlined best in https://lwn.net/Articles/586904/ this uses
|
|
|
+// flock() instead of fcntl(). The ability to lock byte ranges is not necessary
|
|
|
+// and flock() has better behavior.
|
|
|
+func WriteLockedFD(f *os.File, data []byte) (int, error) {
|
|
|
+ // For the reasons outlined best in https://lwn.net/Articles/586904/ we're
|
|
|
+ // going to use flock() instead of fcntl() because we want a whole-file lock.
|
|
|
+ if err := syscall.Flock(int(f.Fd()), syscall.LOCK_EX); err != nil {
|
|
|
+ return 0, fmt.Errorf("unexpected error flock()-ing with EX: %w", err)
|
|
|
+ }
|
|
|
+ defer func() {
|
|
|
+ if err := syscall.Flock(int(f.Fd()), syscall.LOCK_UN); err != nil {
|
|
|
+ log.Errorf("unexpected error flock()-ing FD %d with UN after writing: %s", f.Fd(), err)
|
|
|
+ }
|
|
|
+ }()
|
|
|
+
|
|
|
+ if err := f.Truncate(0); err != nil {
|
|
|
+ return 0, fmt.Errorf("truncating: %w", err)
|
|
|
+ }
|
|
|
+
|
|
|
+ if _, err := f.Seek(0, 0); err != nil {
|
|
|
+ return 0, fmt.Errorf("seeking to 0 before write: %w", err)
|
|
|
+ }
|
|
|
+ defer func() {
|
|
|
+ if _, err := f.Seek(0, 0); err != nil {
|
|
|
+ log.Errorf("unexpected error seeking to 0 after write on FD %d: %s", f.Fd(), err)
|
|
|
+ }
|
|
|
+ }()
|
|
|
+
|
|
|
+ n, err := f.Write(data)
|
|
|
+ if err != nil {
|
|
|
+ return n, fmt.Errorf("writing data: %w", err)
|
|
|
+ }
|
|
|
+
|
|
|
+ return n, nil
|
|
|
+}
|
|
|
+
|
|
|
+// WriteLocked opens the file and then calls WriteLockedFD.
|
|
|
+func WriteLocked(filename string, data []byte) (int, error) {
|
|
|
+ file, err := os.OpenFile(filename, os.O_RDWR|os.O_CREATE, 0600)
|
|
|
+ if err != nil {
|
|
|
+ return 0, fmt.Errorf("opening %s: %w", filename, err)
|
|
|
+ }
|
|
|
+ defer file.Close()
|
|
|
+
|
|
|
+ return WriteLockedFD(file, data)
|
|
|
+}
|
|
|
+
|
|
|
+// ReadLockedFD uses the flock() syscall to safely read from an open file as
|
|
|
+// long as other users of the file are also using flock()-based access.
|
|
|
+//
|
|
|
+// ReadLockedFD will block until it gets lock access.
|
|
|
+//
|
|
|
+// This will read the file in full, from 0, regardless of the current
|
|
|
+// position and then reset the position to 0.
|
|
|
+//
|
|
|
+// For the reasons outlined best in https://lwn.net/Articles/586904/ this uses
|
|
|
+// flock() instead of fcntl(). The ability to lock byte ranges is not necessary
|
|
|
+// and flock() has better behavior.
|
|
|
+func ReadLockedFD(f *os.File) ([]byte, error) {
|
|
|
+ // For the reasons outlined best in https://lwn.net/Articles/586904/ we're
|
|
|
+ // going to use flock() instead of fcntl() because we want a whole-file lock.
|
|
|
+ if err := syscall.Flock(int(f.Fd()), syscall.LOCK_SH); err != nil {
|
|
|
+ return nil, fmt.Errorf("unexpected error flock()-ing with SH: %w", err)
|
|
|
+ }
|
|
|
+ defer func() {
|
|
|
+ if err := syscall.Flock(int(f.Fd()), syscall.LOCK_UN); err != nil {
|
|
|
+ log.Errorf("unexpected error flock()-ing FD %d with UN reading: %s", f.Fd(), err)
|
|
|
+ }
|
|
|
+ }()
|
|
|
+
|
|
|
+ if _, err := f.Seek(0, 0); err != nil {
|
|
|
+ return nil, fmt.Errorf("seeking to 0 before read: %w", err)
|
|
|
+ }
|
|
|
+ defer func() {
|
|
|
+ if _, err := f.Seek(0, 0); err != nil {
|
|
|
+ log.Errorf("unexpected error seeking to 0 after read on FD %d: %s", f.Fd(), err)
|
|
|
+ }
|
|
|
+ }()
|
|
|
+
|
|
|
+ buf := bytes.NewBuffer(nil)
|
|
|
+ if _, err := io.Copy(buf, f); err != nil {
|
|
|
+ if err := syscall.Flock(int(f.Fd()), syscall.LOCK_UN); err != nil {
|
|
|
+ log.Errorf("unexpected error flock()-ing with UN after error reading: %s", err)
|
|
|
+ }
|
|
|
+ return nil, fmt.Errorf("copying data out of file: %w", err)
|
|
|
+ }
|
|
|
+
|
|
|
+ return buf.Bytes(), nil
|
|
|
+}
|
|
|
+
|
|
|
+// ReadLocked opens the given file and then calls ReadLockedFD.
|
|
|
+func ReadLocked(filename string) ([]byte, error) {
|
|
|
+ file, err := os.OpenFile(filename, os.O_RDONLY, 0600)
|
|
|
+ if err != nil {
|
|
|
+ return nil, fmt.Errorf("opening %s: %w", filename, err)
|
|
|
+ }
|
|
|
+ defer file.Close()
|
|
|
+
|
|
|
+ return ReadLockedFD(file)
|
|
|
+}
|