You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 
 

218 lines
5.0 KiB

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 {
log.Fatal(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 {
log.Fatal(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
}
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
}