File: tie_file.tcl

package info (click to toggle)
tcllib 2.0%2Bdfsg-4
  • links: PTS
  • area: main
  • in suites: trixie
  • size: 83,572 kB
  • sloc: tcl: 306,798; ansic: 14,272; sh: 3,035; xml: 1,766; yacc: 1,157; pascal: 881; makefile: 124; perl: 84; f90: 84; python: 33; ruby: 13; php: 11
file content (272 lines) | stat: -rw-r--r-- 6,664 bytes parent folder | download | duplicates (2)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
# tie_file.tcl --
#
#	Data source: Files.
#
# Copyright (c) 2004-2021 Andreas Kupries <andreas_kupries@users.sourceforge.net>
#
# See the file "license.terms" for information on usage and redistribution
# of this file, and for a DISCLAIMER OF ALL WARRANTIES.

# ### ### ### ######### ######### #########
## Requisites

package require Tcl 8.5 9
package require snit
package require tie

# ### ### ### ######### ######### #########
## Implementation

snit::type ::tie::std::file {
    # ### ### ### ######### ######### #########
    ## Notes

    ## This data source maintains an internal cache for higher
    ## efficiency, i.e. to avoid having to go out to the slow file.

    ## This cache is handled as follows
    ##
    ## - All write operations invalidate the cache and write directly
    ##   to the file.
    ##
    ## - All read operations load from the file if the cache is
    ##   invalid, and from the cache otherwise

    ## This scheme works well in the following situations:

    ## (a) The data source is created, and then only read from.
    ## (b) The data source is created, and then only written to.
    ## (c) The data source is created, read once, and then only
    ##     written to.

    ## This scheme works badly if the data source is opened and then
    ## randomly read from and written to. The cache is useless, as it
    ## is continuously invalidated and reloaded.

    ## This no problem from this developers POV of view however.
    ## Consider the context. If you have this situation just tie the
    ## DS to an array A after creation. The tie framework operates on
    ## the DS in mode (c) and A becomes an explicit cache for the DS
    ## which is not invalidated by writing to it. IOW this covers
    ## exactly the situation the DS by itself is not working well for.

    # ### ### ### ######### ######### #########
    ## Specials

    pragma -hastypemethods no
    pragma -hasinfo        no
    pragma -simpledispatch yes

    # ### ### ### ######### ######### #########
    ## API : Construction & Destruction

    constructor {thepath} {
	# Locate and open the journal file.

	set path [::file normalize $thepath]
	if {[::file exists $path]} {
	    set chan [open $path {RDWR EXCL APPEND}]
	} else {
	    set chan [open $path {RDWR EXCL CREAT APPEND}]
	}
	fconfigure $chan -buffering none -encoding utf-8
	return
    }

    destructor {
	# Release the channel to the journal file, should it be open.
	if {$chan ne ""} {close $chan}
	return
    }

    # ### ### ### ######### ######### #########
    ## API : Data source methods

    method get {} {
	if {![::file size $path]} {return {}}
	$self LoadJournal
	return [array get cache]
    }

    method set {dict} {
	puts $chan [list array set $dict]
	$self Invalidate
	return
    }

    method unset {{pattern *}} {
	puts $chan [list array unset $pattern]
	$self Invalidate
	return
    }

    method names {} {
	if {![::file size $path]} {return {}}
	$self LoadJournal
	return [array names cache]
    }

    method size {} {
	if {![::file size $path]} {return 0}
	$self LoadJournal
	return [array size cache]
    }

    method getv {index} {
	if {![::file size $path]} {
	    return -code error "can't read \"$index\": no such variable"
	}
	$self LoadJournal
	return $cache($index)
    }

    method setv {index value} {
	puts $chan [list set $index $value]
	$self Invalidate
	return
    }

    method unsetv {index} {
	puts $chan [list unset $index]
	$self Invalidate
	return
    }

    # ### ### ### ######### ######### #########
    ## Internal : Instance data

    variable chan {} ; # Channel to write the journal.
    variable path {} ; # Path to journal file.

    # Journal loading, and cache.

    variable count 0         ; # #Operations in the journal.
    variable cvalid 0        ; # Validity of the cache.
    variable cache -array {} ; # Cache for journal

    # Management of the cache: See notes at beginning.

    # ### ### ### ######### ######### #########
    ## Internal: Loading from the journal.

    method LoadJournal {} {
	if {$cvalid} return
	$self Replay
	$self Compact
	return
    }

    method Replay {} {
	# Use a safe interp for the evaluation of the journal file.
	# (Empty safe for the hidden commands and the aliases we insert).

	# Called for !cvalid, implies cache does not exist

	set ip [interp create -safe]
	foreach c [$ip eval {info commands}] {
	    if {$c eq "rename"} continue
	    $ip eval [list rename $c {}]
	}
	$ip eval {rename rename {}}

	interp alias $ip set   {} $self Set
	interp alias $ip unset {} $self Unset
	interp alias $ip array {} $self Array

	array set cache {}
	set       count 0

	set jchan [open $path r]
	fconfigure $jchan -encoding utf-8
	set data [read $jchan]
	close $jchan

	$ip eval $data
	interp delete $ip

	set cvalid 1
	return
    }

    method Compact {} {
	# Compact the journal

	#puts @@/2*$count/3*[array size temp]/=/[expr {2*$count >= 3*[array size temp]}]

	# ASSERT cvalid

	# do not compact <=>
	# 2*ops < 3*size <=>
	# ops < 3/2*size <=>
	# ops < 1.5*size

	if {(2*$count) < (3*[array size cache])} return

	::file delete -force ${path}.new
	set new [open ${path}.new {RDWR EXCL CREAT APPEND}]
	fconfigure $new -buffering none -encoding utf-8

	# Compress current contents into a single multi-key load operation.
	puts $new [list array set [array get cache]]

	if {$::tcl_platform(platform) eq "windows"} {
	    # For windows the open channels prevent us from
	    # overwriting the old file. We have to leave
	    # attackers a (small) window of opportunity for
	    # replacing the file with something they own :(
	    close $chan
	    close $new
	    ::file rename -force ${path}.new $path
	    set chan [open ${path} {RDWR EXCL APPEND}]
	    fconfigure $chan -buffering none -encoding utf-8
	} else {
	    # Copy compacted journal over the existing one.
	    ::file rename -force ${path}.new $path
	    close $chan
	    set    chan $new
	}
	return
    }

    method Set {index value} {
	set cache($index) $value
	incr count
	return
    }

    method Unset {index} {
	unset cache($index)
	incr count
	return
    }

    method Array {cmd detail} {
	# syntax : set   dict
	# ...... : unset pattern

	if {$cmd eq "set"} {
	    array set cache $detail
	} elseif {$cmd eq "unset"} {
	    array unset cache $detail
	} else {
	    return -code error "Illegal command \"$cmd\""
	}
	incr count
	return
    }

    method Invalidate {} {
	if {!$cvalid} return
	set cvalid 0
	unset cache
	return
    }

    # ### ### ### ######### ######### #########
}

# ### ### ### ######### ######### #########
## Ready to go

::tie::register ::tie::std::file as file
package provide   tie::std::file 1.2