| 1 | # tie_file.tcl -- |
|---|
| 2 | # |
|---|
| 3 | # Data source: Files. |
|---|
| 4 | # |
|---|
| 5 | # Copyright (c) 2004 Andreas Kupries <andreas_kupries@users.sourceforge.net> |
|---|
| 6 | # |
|---|
| 7 | # See the file "license.terms" for information on usage and redistribution |
|---|
| 8 | # of this file, and for a DISCLAIMER OF ALL WARRANTIES. |
|---|
| 9 | # |
|---|
| 10 | # RCS: @(#) $Id: tie_file.tcl,v 1.8 2006/09/19 23:36:18 andreas_kupries Exp $ |
|---|
| 11 | |
|---|
| 12 | # ### ### ### ######### ######### ######### |
|---|
| 13 | ## Requisites |
|---|
| 14 | |
|---|
| 15 | package require snit |
|---|
| 16 | package require tie |
|---|
| 17 | |
|---|
| 18 | # ### ### ### ######### ######### ######### |
|---|
| 19 | ## Implementation |
|---|
| 20 | |
|---|
| 21 | snit::type ::tie::std::file { |
|---|
| 22 | # ### ### ### ######### ######### ######### |
|---|
| 23 | ## Notes |
|---|
| 24 | |
|---|
| 25 | ## This data source maintains an internal cache for higher |
|---|
| 26 | ## efficiency, i.e. to avoid having to go out to the slow file. |
|---|
| 27 | |
|---|
| 28 | ## This cache is handled as follows |
|---|
| 29 | ## |
|---|
| 30 | ## - All write operations invalidate the cache and write directly |
|---|
| 31 | ## to the file. |
|---|
| 32 | ## |
|---|
| 33 | ## - All read operations load from the file if the cache is |
|---|
| 34 | ## invalid, and from the cache otherwise |
|---|
| 35 | |
|---|
| 36 | ## This scheme works well in the following situations: |
|---|
| 37 | |
|---|
| 38 | ## (a) The data source is created, and then only read from. |
|---|
| 39 | ## (b) The data source is created, and then only written to. |
|---|
| 40 | ## (c) The data source is created, read once, and then only |
|---|
| 41 | ## written to. |
|---|
| 42 | |
|---|
| 43 | ## This scheme works badly if the data source is opened and then |
|---|
| 44 | ## randomly read from and written to. The cache is useless, as it |
|---|
| 45 | ## is continuously invalidated and reloaded. |
|---|
| 46 | |
|---|
| 47 | ## This no problem from this developers POV of view however. |
|---|
| 48 | ## Consider the context. If you have this situation just tie the |
|---|
| 49 | ## DS to an array A after creation. The tie framework operates on |
|---|
| 50 | ## the DS in mode (c) and A becomes an explicit cache for the DS |
|---|
| 51 | ## which is not invalidated by writing to it. IOW this covers |
|---|
| 52 | ## exactly the situation the DS by itself is not working well for. |
|---|
| 53 | |
|---|
| 54 | # ### ### ### ######### ######### ######### |
|---|
| 55 | ## Specials |
|---|
| 56 | |
|---|
| 57 | pragma -hastypemethods no |
|---|
| 58 | pragma -hasinfo no |
|---|
| 59 | pragma -simpledispatch yes |
|---|
| 60 | |
|---|
| 61 | # ### ### ### ######### ######### ######### |
|---|
| 62 | ## API : Construction & Destruction |
|---|
| 63 | |
|---|
| 64 | constructor {thepath} { |
|---|
| 65 | # Locate and open the journal file. |
|---|
| 66 | |
|---|
| 67 | set path [::file normalize $thepath] |
|---|
| 68 | if {[::file exists $path]} { |
|---|
| 69 | set chan [open $path {RDWR EXCL APPEND}] |
|---|
| 70 | } else { |
|---|
| 71 | set chan [open $path {RDWR EXCL CREAT APPEND}] |
|---|
| 72 | } |
|---|
| 73 | fconfigure $chan -buffering none -encoding utf-8 |
|---|
| 74 | return |
|---|
| 75 | } |
|---|
| 76 | |
|---|
| 77 | destructor { |
|---|
| 78 | # Release the channel to the journal file, should it be open. |
|---|
| 79 | if {$chan ne ""} {close $chan} |
|---|
| 80 | return |
|---|
| 81 | } |
|---|
| 82 | |
|---|
| 83 | # ### ### ### ######### ######### ######### |
|---|
| 84 | ## API : Data source methods |
|---|
| 85 | |
|---|
| 86 | method get {} { |
|---|
| 87 | if {![::file size $path]} {return {}} |
|---|
| 88 | $self LoadJournal |
|---|
| 89 | return [array get cache] |
|---|
| 90 | } |
|---|
| 91 | |
|---|
| 92 | method set {dict} { |
|---|
| 93 | puts $chan [list array set $dict] |
|---|
| 94 | $self Invalidate |
|---|
| 95 | return |
|---|
| 96 | } |
|---|
| 97 | |
|---|
| 98 | method unset {{pattern *}} { |
|---|
| 99 | puts $chan [list array unset $pattern] |
|---|
| 100 | $self Invalidate |
|---|
| 101 | return |
|---|
| 102 | } |
|---|
| 103 | |
|---|
| 104 | method names {} { |
|---|
| 105 | if {![::file size $path]} {return {}} |
|---|
| 106 | $self LoadJournal |
|---|
| 107 | return [array names cache] |
|---|
| 108 | } |
|---|
| 109 | |
|---|
| 110 | method size {} { |
|---|
| 111 | if {![::file size $path]} {return 0} |
|---|
| 112 | $self LoadJournal |
|---|
| 113 | return [array size cache] |
|---|
| 114 | } |
|---|
| 115 | |
|---|
| 116 | method getv {index} { |
|---|
| 117 | if {![::file size $path]} { |
|---|
| 118 | return -code error "can't read \"$index\": no such variable" |
|---|
| 119 | } |
|---|
| 120 | $self LoadJournal |
|---|
| 121 | return $cache($index) |
|---|
| 122 | } |
|---|
| 123 | |
|---|
| 124 | method setv {index value} { |
|---|
| 125 | puts $chan [list set $index $value] |
|---|
| 126 | $self Invalidate |
|---|
| 127 | return |
|---|
| 128 | } |
|---|
| 129 | |
|---|
| 130 | method unsetv {index} { |
|---|
| 131 | puts $chan [list unset $index] |
|---|
| 132 | $self Invalidate |
|---|
| 133 | return |
|---|
| 134 | } |
|---|
| 135 | |
|---|
| 136 | # ### ### ### ######### ######### ######### |
|---|
| 137 | ## Internal : Instance data |
|---|
| 138 | |
|---|
| 139 | variable chan {} ; # Channel to write the journal. |
|---|
| 140 | variable path {} ; # Path to journal file. |
|---|
| 141 | |
|---|
| 142 | # Journal loading, and cache. |
|---|
| 143 | |
|---|
| 144 | variable count 0 ; # #Operations in the journal. |
|---|
| 145 | variable cvalid 0 ; # Validity of the cache. |
|---|
| 146 | variable cache -array {} ; # Cache for journal |
|---|
| 147 | |
|---|
| 148 | # Management of the cache: See notes at beginning. |
|---|
| 149 | |
|---|
| 150 | # ### ### ### ######### ######### ######### |
|---|
| 151 | ## Internal: Loading from the journal. |
|---|
| 152 | |
|---|
| 153 | method LoadJournal {} { |
|---|
| 154 | if {$cvalid} return |
|---|
| 155 | $self Replay |
|---|
| 156 | $self Compact |
|---|
| 157 | return |
|---|
| 158 | } |
|---|
| 159 | |
|---|
| 160 | method Replay {} { |
|---|
| 161 | # Use a safe interp for the evaluation of the journal file. |
|---|
| 162 | # (Empty safe for the hidden commands and the aliases we insert). |
|---|
| 163 | |
|---|
| 164 | # Called for !cvalid, implies cache does not exist |
|---|
| 165 | |
|---|
| 166 | set ip [interp create -safe] |
|---|
| 167 | foreach c [$ip eval {info commands}] { |
|---|
| 168 | if {$c eq "rename"} continue |
|---|
| 169 | $ip eval [list rename $c {}] |
|---|
| 170 | } |
|---|
| 171 | $ip eval {rename rename {}} |
|---|
| 172 | |
|---|
| 173 | interp alias $ip set {} $self Set |
|---|
| 174 | interp alias $ip unset {} $self Unset |
|---|
| 175 | interp alias $ip array {} $self Array |
|---|
| 176 | |
|---|
| 177 | array set cache {} |
|---|
| 178 | set count 0 |
|---|
| 179 | |
|---|
| 180 | $ip invokehidden -global source $path |
|---|
| 181 | interp delete $ip |
|---|
| 182 | |
|---|
| 183 | set cvalid 1 |
|---|
| 184 | return |
|---|
| 185 | } |
|---|
| 186 | |
|---|
| 187 | method Compact {} { |
|---|
| 188 | # Compact the journal |
|---|
| 189 | |
|---|
| 190 | #puts @@/2*$count/3*[array size temp]/=/[expr {2*$count >= 3*[array size temp]}] |
|---|
| 191 | |
|---|
| 192 | # ASSERT cvalid |
|---|
| 193 | |
|---|
| 194 | # do not compact <=> |
|---|
| 195 | # 2*ops < 3*size <=> |
|---|
| 196 | # ops < 3/2*size <=> |
|---|
| 197 | # ops < 1.5*size |
|---|
| 198 | |
|---|
| 199 | if {(2*$count) < (3*[array size cache])} return |
|---|
| 200 | |
|---|
| 201 | ::file delete -force ${path}.new |
|---|
| 202 | set new [open ${path}.new {RDWR EXCL CREAT APPEND}] |
|---|
| 203 | fconfigure $new -buffering none -encoding utf-8 |
|---|
| 204 | |
|---|
| 205 | # Compress current contents into a single multi-key load operation. |
|---|
| 206 | puts $new [list array set [array get cache]] |
|---|
| 207 | |
|---|
| 208 | if {$::tcl_platform(platform) eq "windows"} { |
|---|
| 209 | # For windows the open channels prevent us from |
|---|
| 210 | # overwriting the old file. We have to leave |
|---|
| 211 | # attackers a (small) window of opportunity for |
|---|
| 212 | # replacing the file with something they own :( |
|---|
| 213 | close $chan |
|---|
| 214 | close $new |
|---|
| 215 | ::file rename -force ${path}.new $path |
|---|
| 216 | set chan [open ${path} {RDWR EXCL APPEND}] |
|---|
| 217 | fconfigure $chan -buffering none -encoding utf-8 |
|---|
| 218 | } else { |
|---|
| 219 | # Copy compacted journal over the existing one. |
|---|
| 220 | ::file rename -force ${path}.new $path |
|---|
| 221 | close $chan |
|---|
| 222 | set chan $new |
|---|
| 223 | } |
|---|
| 224 | return |
|---|
| 225 | } |
|---|
| 226 | |
|---|
| 227 | method Set {index value} { |
|---|
| 228 | set cache($index) $value |
|---|
| 229 | incr count |
|---|
| 230 | return |
|---|
| 231 | } |
|---|
| 232 | |
|---|
| 233 | method Unset {index} { |
|---|
| 234 | unset cache($index) |
|---|
| 235 | incr count |
|---|
| 236 | return |
|---|
| 237 | } |
|---|
| 238 | |
|---|
| 239 | method Array {cmd detail} { |
|---|
| 240 | # syntax : set dict |
|---|
| 241 | # ...... : unset pattern |
|---|
| 242 | |
|---|
| 243 | if {$cmd eq "set"} { |
|---|
| 244 | array set cache $detail |
|---|
| 245 | } elseif {$cmd eq "unset"} { |
|---|
| 246 | array unset cache $detail |
|---|
| 247 | } else { |
|---|
| 248 | return -code error "Illegal command \"$cmd\"" |
|---|
| 249 | } |
|---|
| 250 | incr count |
|---|
| 251 | return |
|---|
| 252 | } |
|---|
| 253 | |
|---|
| 254 | method Invalidate {} { |
|---|
| 255 | if {!$cvalid} return |
|---|
| 256 | set cvalid 0 |
|---|
| 257 | unset cache |
|---|
| 258 | return |
|---|
| 259 | } |
|---|
| 260 | |
|---|
| 261 | # ### ### ### ######### ######### ######### |
|---|
| 262 | } |
|---|
| 263 | |
|---|
| 264 | # ### ### ### ######### ######### ######### |
|---|
| 265 | ## Ready to go |
|---|
| 266 | |
|---|
| 267 | ::tie::register ::tie::std::file as file |
|---|
| 268 | package provide tie::std::file 1.0.2 |
|---|