github.com/psiphon-labs/psiphon-tunnel-core@v2.0.28+incompatible/psiphon/common/reloader.go (about)

     1  /*
     2   * Copyright (c) 2016, Psiphon Inc.
     3   * All rights reserved.
     4   *
     5   * This program is free software: you can redistribute it and/or modify
     6   * it under the terms of the GNU General Public License as published by
     7   * the Free Software Foundation, either version 3 of the License, or
     8   * (at your option) any later version.
     9   *
    10   * This program is distributed in the hope that it will be useful,
    11   * but WITHOUT ANY WARRANTY; without even the implied warranty of
    12   * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    13   * GNU General Public License for more details.
    14   *
    15   * You should have received a copy of the GNU General Public License
    16   * along with this program.  If not, see <http://www.gnu.org/licenses/>.
    17   *
    18   */
    19  
    20  package common
    21  
    22  import (
    23  	"hash/crc64"
    24  	"io"
    25  	"io/ioutil"
    26  	"os"
    27  	"sync"
    28  	"time"
    29  
    30  	"github.com/Psiphon-Labs/psiphon-tunnel-core/psiphon/common/errors"
    31  )
    32  
    33  // Reloader represents a read-only, in-memory reloadable data object. For example,
    34  // a JSON data file that is loaded into memory and accessed for read-only lookups;
    35  // and from time to time may be reloaded from the same file, updating the memory
    36  // copy.
    37  type Reloader interface {
    38  
    39  	// Reload reloads the data object. Reload returns a flag indicating if the
    40  	// reloadable target has changed and reloaded or remains unchanged. By
    41  	// convention, when reloading fails the Reloader should revert to its previous
    42  	// in-memory state.
    43  	Reload() (bool, error)
    44  
    45  	// WillReload indicates if the data object is capable of reloading.
    46  	WillReload() bool
    47  
    48  	// LogDescription returns a description to be used for logging
    49  	// events related to the Reloader.
    50  	LogDescription() string
    51  }
    52  
    53  // ReloadableFile is a file-backed Reloader. This type is intended to be embedded
    54  // in other types that add the actual reloadable data structures.
    55  //
    56  // ReloadableFile has a multi-reader mutex for synchronization. Its Reload() function
    57  // will obtain a write lock before reloading the data structures. The actual reloading
    58  // action is to be provided via the reloadAction callback, which receives the content
    59  // of reloaded files, along with file modification time, and must process the new data
    60  // (for example, unmarshall the contents into data structures). All read access to the
    61  // data structures should be guarded by RLocks on the ReloadableFile mutex.
    62  //
    63  // reloadAction must ensure that data structures revert to their previous state when
    64  // a reload fails.
    65  //
    66  type ReloadableFile struct {
    67  	sync.RWMutex
    68  	filename        string
    69  	loadFileContent bool
    70  	checksum        uint64
    71  	reloadAction    func([]byte, time.Time) error
    72  }
    73  
    74  // NewReloadableFile initializes a new ReloadableFile.
    75  //
    76  // When loadFileContent is true, the file content is loaded and passed to
    77  // reloadAction; otherwise, reloadAction receives a nil argument and is
    78  // responsible for loading the file. The latter option allows for cases where
    79  // the file contents must be streamed, memory mapped, etc.
    80  func NewReloadableFile(
    81  	filename string,
    82  	loadFileContent bool,
    83  	reloadAction func([]byte, time.Time) error) ReloadableFile {
    84  
    85  	return ReloadableFile{
    86  		filename:        filename,
    87  		loadFileContent: loadFileContent,
    88  		reloadAction:    reloadAction,
    89  	}
    90  }
    91  
    92  // WillReload indicates whether the ReloadableFile is capable
    93  // of reloading.
    94  func (reloadable *ReloadableFile) WillReload() bool {
    95  	return reloadable.filename != ""
    96  }
    97  
    98  var crc64table = crc64.MakeTable(crc64.ISO)
    99  
   100  // Reload checks if the underlying file has changed and, when changed, invokes
   101  // the reloadAction callback which should reload the in-memory data structures.
   102  //
   103  // In some case (e.g., traffic rules and OSL), there are penalties associated
   104  // with proceeding with reload, so care is taken to not invoke the reload action
   105  // unless the contents have changed.
   106  //
   107  // The file content is loaded and a checksum is taken to determine whether it
   108  // has changed. Neither file size (may not change when content changes) nor
   109  // modified date (may change when identical file is repaved) is a sufficient
   110  // indicator.
   111  //
   112  // All data structure readers should be blocked by the ReloadableFile mutex.
   113  //
   114  // Reload must not be called from multiple concurrent goroutines.
   115  func (reloadable *ReloadableFile) Reload() (bool, error) {
   116  
   117  	if !reloadable.WillReload() {
   118  		return false, nil
   119  	}
   120  
   121  	// Check whether the file has changed _before_ blocking readers
   122  
   123  	reloadable.RLock()
   124  	filename := reloadable.filename
   125  	previousChecksum := reloadable.checksum
   126  	reloadable.RUnlock()
   127  
   128  	// Record the file modification time _before_ loading, as reload actions will
   129  	// assume that the content is at least as fresh as the modification time.
   130  	fileInfo, err := os.Stat(filename)
   131  	if err != nil {
   132  		return false, errors.Trace(err)
   133  	}
   134  	fileModTime := fileInfo.ModTime()
   135  
   136  	file, err := os.Open(filename)
   137  	if err != nil {
   138  		return false, errors.Trace(err)
   139  	}
   140  	defer file.Close()
   141  
   142  	hash := crc64.New(crc64table)
   143  
   144  	_, err = io.Copy(hash, file)
   145  	if err != nil {
   146  		return false, errors.Trace(err)
   147  	}
   148  
   149  	checksum := hash.Sum64()
   150  
   151  	if checksum == previousChecksum {
   152  		return false, nil
   153  	}
   154  
   155  	// It's possible for the file content to revert to its previous value
   156  	// between the checksum operation and subsequent content load. We accept
   157  	// the false positive in this unlikely case.
   158  
   159  	var content []byte
   160  	if reloadable.loadFileContent {
   161  		_, err = file.Seek(0, 0)
   162  		if err != nil {
   163  			return false, errors.Trace(err)
   164  		}
   165  		content, err = ioutil.ReadAll(file)
   166  		if err != nil {
   167  			return false, errors.Trace(err)
   168  		}
   169  	}
   170  
   171  	// Don't keep file open during reloadAction call.
   172  	file.Close()
   173  
   174  	// ...now block readers and reload
   175  
   176  	reloadable.Lock()
   177  	defer reloadable.Unlock()
   178  
   179  	err = reloadable.reloadAction(content, fileModTime)
   180  	if err != nil {
   181  		return false, errors.Trace(err)
   182  	}
   183  
   184  	reloadable.checksum = checksum
   185  
   186  	return true, nil
   187  }
   188  
   189  func (reloadable *ReloadableFile) LogDescription() string {
   190  	return reloadable.filename
   191  }