github.com/wtsi-ssg/wrstat/v3@v3.2.3/watch/watch.go (about)

     1  /*******************************************************************************
     2   * Copyright (c) 2022 Genome Research Ltd.
     3   *
     4   * Author: Sendu Bala <sb10@sanger.ac.uk>
     5   *
     6   * Permission is hereby granted, free of charge, to any person obtaining
     7   * a copy of this software and associated documentation files (the
     8   * "Software"), to deal in the Software without restriction, including
     9   * without limitation the rights to use, copy, modify, merge, publish,
    10   * distribute, sublicense, and/or sell copies of the Software, and to
    11   * permit persons to whom the Software is furnished to do so, subject to
    12   * the following conditions:
    13   *
    14   * The above copyright notice and this permission notice shall be included
    15   * in all copies or substantial portions of the Software.
    16   *
    17   * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
    18   * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
    19   * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
    20   * IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
    21   * CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
    22   * TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
    23   * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
    24   ******************************************************************************/
    25  
    26  // package watch is used to watch single files for changes to their mtimes, for
    27  // filesystems that don't support inotify.
    28  
    29  package watch
    30  
    31  import (
    32  	"os"
    33  	"sync"
    34  	"time"
    35  )
    36  
    37  // WatcherCallback, once supplied to New(), will be called each time your
    38  // Watcher path changes (that is, the file's mtime changes), and is supplied the
    39  // new mtime of that path.
    40  type WatcherCallback func(mtime time.Time)
    41  
    42  // Watcher is used to watch a file on a filesystem, and let you do something
    43  // whenever it's mtime changes.
    44  type Watcher struct {
    45  	path          string
    46  	cb            WatcherCallback
    47  	pollFrequency time.Duration
    48  	previous      time.Time
    49  	stop          chan bool
    50  	stopped       bool
    51  	sync.RWMutex
    52  }
    53  
    54  // New returns a new Watcher that will call your cb with path's mtime whenever
    55  // its mtime changes in the future.
    56  //
    57  // It also immediately gets the path's mtime, available via Mtime().
    58  //
    59  // This is intended for use on filesystems that don't support inotify, so the
    60  // watcher will check for changes to path's mtime every pollFrequency.
    61  func New(path string, cb WatcherCallback, pollFrequency time.Duration) (*Watcher, error) {
    62  	mtime, err := getFileMtime(path)
    63  	if err != nil {
    64  		return nil, err
    65  	}
    66  
    67  	w := &Watcher{
    68  		path:          path,
    69  		cb:            cb,
    70  		pollFrequency: pollFrequency,
    71  		previous:      mtime,
    72  	}
    73  
    74  	w.startWatching()
    75  
    76  	return w, nil
    77  }
    78  
    79  // getFileMtime returns the mtime of the given file.
    80  func getFileMtime(path string) (time.Time, error) {
    81  	info, err := os.Stat(path)
    82  	if err != nil {
    83  		return time.Time{}, err
    84  	}
    85  
    86  	return info.ModTime(), nil
    87  }
    88  
    89  // startWatching will start ticking at our pollFrequency, and call our cb if
    90  // our path's mtime changes.
    91  func (w *Watcher) startWatching() {
    92  	ticker := time.NewTicker(w.pollFrequency)
    93  
    94  	stopTicking := make(chan bool)
    95  
    96  	w.stop = stopTicking
    97  
    98  	go func() {
    99  		defer ticker.Stop()
   100  
   101  		for {
   102  			select {
   103  			case <-stopTicking:
   104  				return
   105  			case <-ticker.C:
   106  				w.callCBIfMtimeChanged()
   107  			}
   108  		}
   109  	}()
   110  }
   111  
   112  // callCBIfMtimeChanged calls our cb if the mtime of our path has changed since
   113  // the last time this method was called. Errors in trying to get the mtime are
   114  // ignored, in the hopes a future attempt will succeed.
   115  func (w *Watcher) callCBIfMtimeChanged() {
   116  	w.Lock()
   117  	defer w.Unlock()
   118  
   119  	mtime, err := getFileMtime(w.path)
   120  	if err != nil || mtime == w.previous {
   121  		return
   122  	}
   123  
   124  	w.cb(mtime)
   125  
   126  	w.previous = mtime
   127  }
   128  
   129  // Mtime returns the latest mtime of our path, captured during New() or the last
   130  // time we polled.
   131  func (w *Watcher) Mtime() time.Time {
   132  	w.RLock()
   133  	defer w.RUnlock()
   134  
   135  	return w.previous
   136  }
   137  
   138  // Stop will stop watching our path for changes.
   139  func (w *Watcher) Stop() {
   140  	w.Lock()
   141  	defer w.Unlock()
   142  
   143  	if w.stopped {
   144  		return
   145  	}
   146  
   147  	close(w.stop)
   148  	w.stopped = true
   149  }