- package autofile
-
- import (
- "context"
- "errors"
- "fmt"
- "os"
- "os/signal"
- "path/filepath"
- "sync"
- "syscall"
- "time"
-
- tmrand "github.com/tendermint/tendermint/libs/rand"
- )
-
- /* AutoFile usage
-
- // Create/Append to ./autofile_test
- af, err := OpenAutoFile("autofile_test")
- if err != nil {
- panic(err)
- }
-
- // Stream of writes.
- // During this time, the file may be moved e.g. by logRotate.
- for i := 0; i < 60; i++ {
- af.Write([]byte(Fmt("LOOP(%v)", i)))
- time.Sleep(time.Second)
- }
-
- // Close the AutoFile
- err = af.Close()
- if err != nil {
- panic(err)
- }
- */
-
- const (
- autoFileClosePeriod = 1000 * time.Millisecond
- autoFilePerms = os.FileMode(0600)
- )
-
- // errAutoFileClosed is reported when operations attempt to use an autofile
- // after it has been closed.
- var errAutoFileClosed = errors.New("autofile is closed")
-
- // AutoFile automatically closes and re-opens file for writing. The file is
- // automatically setup to close itself every 1s and upon receiving SIGHUP.
- //
- // This is useful for using a log file with the logrotate tool.
- type AutoFile struct {
- ID string
- Path string
-
- closeTicker *time.Ticker // signals periodic close
- cancel func() // cancels the lifecycle context
-
- mtx sync.Mutex // guards the fields below
- closed bool // true when the the autofile is no longer usable
- file *os.File // the underlying file (may be nil)
- }
-
- // OpenAutoFile creates an AutoFile in the path (with random ID). If there is
- // an error, it will be of type *PathError or *ErrPermissionsChanged (if file's
- // permissions got changed (should be 0600)).
- func OpenAutoFile(ctx context.Context, path string) (*AutoFile, error) {
- var err error
- path, err = filepath.Abs(path)
- if err != nil {
- return nil, err
- }
-
- ctx, cancel := context.WithCancel(ctx)
- af := &AutoFile{
- ID: tmrand.Str(12) + ":" + path,
- Path: path,
- closeTicker: time.NewTicker(autoFileClosePeriod),
- cancel: cancel,
- }
- if err := af.openFile(); err != nil {
- af.Close()
- return nil, err
- }
-
- // Set up a SIGHUP handler to forcibly flush and close the filehandle.
- // This forces the next operation to re-open the underlying path.
- hupc := make(chan os.Signal, 1)
- signal.Notify(hupc, syscall.SIGHUP)
- go func() {
- defer close(hupc)
- for {
- select {
- case <-hupc:
- _ = af.closeFile()
- case <-ctx.Done():
- return
- }
- }
- }()
-
- go af.closeFileRoutine(ctx)
-
- return af, nil
- }
-
- // Close shuts down the service goroutine and marks af as invalid. Operations
- // on af after Close will report an error.
- func (af *AutoFile) Close() error {
- return af.withLock(func() error {
- af.cancel() // signal the close service to stop
- af.closed = true // mark the file as invalid
- return af.unsyncCloseFile()
- })
- }
-
- func (af *AutoFile) closeFileRoutine(ctx context.Context) {
- for {
- select {
- case <-ctx.Done():
- _ = af.Close()
- return
- case <-af.closeTicker.C:
- _ = af.closeFile()
- }
- }
- }
-
- func (af *AutoFile) closeFile() (err error) {
- return af.withLock(af.unsyncCloseFile)
- }
-
- // unsyncCloseFile closes the underlying filehandle if one is open, and reports
- // any error it returns. The caller must hold af.mtx exclusively.
- func (af *AutoFile) unsyncCloseFile() error {
- if fp := af.file; fp != nil {
- af.file = nil
- return fp.Close()
- }
- return nil
- }
-
- // withLock runs f while holding af.mtx, and reports any error it returns.
- func (af *AutoFile) withLock(f func() error) error {
- af.mtx.Lock()
- defer af.mtx.Unlock()
- return f()
- }
-
- // Write writes len(b) bytes to the AutoFile. It returns the number of bytes
- // written and an error, if any. Write returns a non-nil error when n !=
- // len(b).
- // Opens AutoFile if needed.
- func (af *AutoFile) Write(b []byte) (n int, err error) {
- af.mtx.Lock()
- defer af.mtx.Unlock()
- if af.closed {
- return 0, fmt.Errorf("write: %w", errAutoFileClosed)
- }
-
- if af.file == nil {
- if err = af.openFile(); err != nil {
- return
- }
- }
-
- n, err = af.file.Write(b)
- return
- }
-
- // Sync commits the current contents of the file to stable storage. Typically,
- // this means flushing the file system's in-memory copy of recently written
- // data to disk.
- func (af *AutoFile) Sync() error {
- return af.withLock(func() error {
- if af.closed {
- return fmt.Errorf("sync: %w", errAutoFileClosed)
- } else if af.file == nil {
- return nil // nothing to sync
- }
- return af.file.Sync()
- })
- }
-
- // openFile unconditionally replaces af.file with a new filehandle on the path.
- // The caller must hold af.mtx exclusively.
- func (af *AutoFile) openFile() error {
- file, err := os.OpenFile(af.Path, os.O_RDWR|os.O_CREATE|os.O_APPEND, autoFilePerms)
- if err != nil {
- return err
- }
- // fileInfo, err := file.Stat()
- // if err != nil {
- // return err
- // }
- // if fileInfo.Mode() != autoFilePerms {
- // return errors.NewErrPermissionsChanged(file.Name(), fileInfo.Mode(), autoFilePerms)
- // }
- af.file = file
- return nil
- }
-
- // Size returns the size of the AutoFile. It returns -1 and an error if fails
- // get stats or open file.
- // Opens AutoFile if needed.
- func (af *AutoFile) Size() (int64, error) {
- af.mtx.Lock()
- defer af.mtx.Unlock()
- if af.closed {
- return 0, fmt.Errorf("size: %w", errAutoFileClosed)
- }
-
- if af.file == nil {
- if err := af.openFile(); err != nil {
- return -1, err
- }
- }
-
- stat, err := af.file.Stat()
- if err != nil {
- return -1, err
- }
- return stat.Size(), nil
- }
|