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 }