github.com/ari-anchor/sei-tendermint@v0.0.0-20230519144642-dc826b7b56bb/internal/libs/autofile/autofile.go (about)

     1  package autofile
     2  
     3  import (
     4  	"context"
     5  	"errors"
     6  	"fmt"
     7  	"os"
     8  	"os/signal"
     9  	"path/filepath"
    10  	"sync"
    11  	"syscall"
    12  	"time"
    13  
    14  	tmrand "github.com/ari-anchor/sei-tendermint/libs/rand"
    15  )
    16  
    17  /* AutoFile usage
    18  
    19  // Create/Append to ./autofile_test
    20  af, err := OpenAutoFile("autofile_test")
    21  if err != nil {
    22          log.Fatal(err)
    23  }
    24  
    25  // Stream of writes.
    26  // During this time, the file may be moved e.g. by logRotate.
    27  for i := 0; i < 60; i++ {
    28  	af.Write([]byte(Fmt("LOOP(%v)", i)))
    29  	time.Sleep(time.Second)
    30  }
    31  
    32  // Close the AutoFile
    33  err = af.Close()
    34  if err != nil {
    35  	log.Fatal(err)
    36  }
    37  */
    38  
    39  const (
    40  	autoFileClosePeriod = 1000 * time.Millisecond
    41  	autoFilePerms       = os.FileMode(0600)
    42  )
    43  
    44  // ErrAutoFileClosed is reported when operations attempt to use an autofile
    45  // after it has been closed.
    46  var ErrAutoFileClosed = errors.New("autofile is closed")
    47  
    48  // AutoFile automatically closes and re-opens file for writing. The file is
    49  // automatically setup to close itself every 1s and upon receiving SIGHUP.
    50  //
    51  // This is useful for using a log file with the logrotate tool.
    52  type AutoFile struct {
    53  	ID   string
    54  	Path string
    55  
    56  	closeTicker *time.Ticker // signals periodic close
    57  	cancel      func()       // cancels the lifecycle context
    58  
    59  	mtx    sync.Mutex // guards the fields below
    60  	closed bool       // true when the the autofile is no longer usable
    61  	file   *os.File   // the underlying file (may be nil)
    62  }
    63  
    64  // OpenAutoFile creates an AutoFile in the path (with random ID). If there is
    65  // an error, it will be of type *PathError or *ErrPermissionsChanged (if file's
    66  // permissions got changed (should be 0600)).
    67  func OpenAutoFile(ctx context.Context, path string) (*AutoFile, error) {
    68  	var err error
    69  	path, err = filepath.Abs(path)
    70  	if err != nil {
    71  		return nil, err
    72  	}
    73  
    74  	ctx, cancel := context.WithCancel(ctx)
    75  	af := &AutoFile{
    76  		ID:          tmrand.Str(12) + ":" + path,
    77  		Path:        path,
    78  		closeTicker: time.NewTicker(autoFileClosePeriod),
    79  		cancel:      cancel,
    80  	}
    81  	if err := af.openFile(); err != nil {
    82  		af.Close()
    83  		return nil, err
    84  	}
    85  
    86  	// Set up a SIGHUP handler to forcibly flush and close the filehandle.
    87  	// This forces the next operation to re-open the underlying path.
    88  	hupc := make(chan os.Signal, 1)
    89  	signal.Notify(hupc, syscall.SIGHUP)
    90  	go func() {
    91  		defer close(hupc)
    92  		for {
    93  			select {
    94  			case <-hupc:
    95  				_ = af.closeFile()
    96  			case <-ctx.Done():
    97  				return
    98  			}
    99  		}
   100  	}()
   101  
   102  	go af.closeFileRoutine(ctx)
   103  
   104  	return af, nil
   105  }
   106  
   107  // Close shuts down the service goroutine and marks af as invalid.  Operations
   108  // on af after Close will report an error.
   109  func (af *AutoFile) Close() error {
   110  	return af.withLock(func() error {
   111  		af.cancel()      // signal the close service to stop
   112  		af.closed = true // mark the file as invalid
   113  		return af.unsyncCloseFile()
   114  	})
   115  }
   116  
   117  func (af *AutoFile) closeFileRoutine(ctx context.Context) {
   118  	for {
   119  		select {
   120  		case <-ctx.Done():
   121  			_ = af.Close()
   122  			return
   123  		case <-af.closeTicker.C:
   124  			_ = af.closeFile()
   125  		}
   126  	}
   127  }
   128  
   129  func (af *AutoFile) closeFile() (err error) {
   130  	return af.withLock(af.unsyncCloseFile)
   131  }
   132  
   133  // unsyncCloseFile closes the underlying filehandle if one is open, and reports
   134  // any error it returns. The caller must hold af.mtx exclusively.
   135  func (af *AutoFile) unsyncCloseFile() error {
   136  	if fp := af.file; fp != nil {
   137  		af.file = nil
   138  		return fp.Close()
   139  	}
   140  	return nil
   141  }
   142  
   143  // withLock runs f while holding af.mtx, and reports any error it returns.
   144  func (af *AutoFile) withLock(f func() error) error {
   145  	af.mtx.Lock()
   146  	defer af.mtx.Unlock()
   147  	return f()
   148  }
   149  
   150  // Write writes len(b) bytes to the AutoFile. It returns the number of bytes
   151  // written and an error, if any. Write returns a non-nil error when n !=
   152  // len(b).
   153  // Opens AutoFile if needed.
   154  func (af *AutoFile) Write(b []byte) (n int, err error) {
   155  	af.mtx.Lock()
   156  	defer af.mtx.Unlock()
   157  	if af.closed {
   158  		return 0, fmt.Errorf("write: %w", ErrAutoFileClosed)
   159  	}
   160  
   161  	if af.file == nil {
   162  		if err = af.openFile(); err != nil {
   163  			return
   164  		}
   165  	}
   166  
   167  	n, err = af.file.Write(b)
   168  	return
   169  }
   170  
   171  // Sync commits the current contents of the file to stable storage. Typically,
   172  // this means flushing the file system's in-memory copy of recently written
   173  // data to disk.
   174  func (af *AutoFile) Sync() error {
   175  	return af.withLock(func() error {
   176  		if af.closed {
   177  			return fmt.Errorf("sync: %w", ErrAutoFileClosed)
   178  		} else if af.file == nil {
   179  			return nil // nothing to sync
   180  		}
   181  		return af.file.Sync()
   182  	})
   183  }
   184  
   185  // openFile unconditionally replaces af.file with a new filehandle on the path.
   186  // The caller must hold af.mtx exclusively.
   187  func (af *AutoFile) openFile() error {
   188  	file, err := os.OpenFile(af.Path, os.O_RDWR|os.O_CREATE|os.O_APPEND, autoFilePerms)
   189  	if err != nil {
   190  		return err
   191  	}
   192  
   193  	af.file = file
   194  	return nil
   195  }
   196  
   197  // Size returns the size of the AutoFile. It returns -1 and an error if fails
   198  // get stats or open file.
   199  // Opens AutoFile if needed.
   200  func (af *AutoFile) Size() (int64, error) {
   201  	af.mtx.Lock()
   202  	defer af.mtx.Unlock()
   203  	if af.closed {
   204  		return 0, fmt.Errorf("size: %w", ErrAutoFileClosed)
   205  	}
   206  
   207  	if af.file == nil {
   208  		if err := af.openFile(); err != nil {
   209  			return -1, err
   210  		}
   211  	}
   212  
   213  	stat, err := af.file.Stat()
   214  	if err != nil {
   215  		return -1, err
   216  	}
   217  	return stat.Size(), nil
   218  }