Factored out the non-gui stuff from TLC
[tcl-tlc-base.git] / scripts / datasource_chan_backend.itcl
blob7722262d376be38406699fd03a93ac07950e2b88
1 # vim: foldmethod=marker foldmarker=<<<,>>> ft=tcl ts=4 shiftwidth=4
3 # TODO:
4 # Periodic housecleaning routine for recently_deceased trimming
6 class tlc::DSchan_backend {
7 inherit tlc::Baselog
9 constructor {args} {}
10 destructor {}
12 public {
13 variable id_column 0
14 variable comp
15 variable tag
16 variable headers
17 variable dbfile ":memory:"
18 variable persist 0
20 method register_pool {pool {check_cb {}}}
21 method deregister_pool {pool}
22 method add_item {pool item}
23 method change_item {pool id newitem}
24 method remove_item {pool id}
25 method get_item {pool id}
26 method item_count {pool}
27 method id_list {pool}
28 method pool_exists {pool}
29 method id_exists {pool id}
30 method start_init {}
31 method end_init {}
32 method abort_init {}
35 private {
36 variable auth
37 variable pools
38 variable pool_jmids
39 variable general_info_jmid
40 variable db
41 variable _dbfile
42 variable in_init_transaction 0
44 method announce_pool {pool}
45 method announce_new {pool id item}
46 method announce_changed {pool id olditem newitem}
47 method announce_removed {pool id item}
48 method check_pool {pool}
49 method req_handler {auth user seq rest}
50 method pool_chan_cb {pool op data}
51 method general_info_chan_cb {op data}
52 method init_db {}
57 configbody tlc::DSchan_backend::headers { #<<<1
58 if {[info exists general_info_jmid]} {
59 $auth jm $general_info_jmid [list headers_changed $headers]
64 configbody tlc::DSchan_backend::id_column { #<<<1
65 if {[info exists general_info_jmid]} {
66 $auth jm $general_info_jmid [list id_column_changed $id_column]
71 body tlc::DSchan_backend::constructor {args} { #<<<1
72 package require sqlite3
74 array set pools {}
75 array set pool_jmids {}
77 eval configure $args
79 foreach reqf {comp tag headers} {
80 if {![info exists $reqf]} {
81 error "Must set -$reqf"
85 # Canonise dbfile <<<
86 if {$dbfile == ":memory:" || $dbfile == ""} {
87 set _dbfile ":memory:"
88 } else {
89 if {![file exists $dbfile]} {
90 set file_hack 1
91 set fp [open $dbfile w]
92 close $fp
94 set _dbfile [file normalize $dbfile]
95 while {[file type $_dbfile] == "link"} {
96 set _dbfile [file readlink $_dbfile]
97 set _dbfile [file normalize $_dbfile]
99 if {[info exists file_hack]} {
100 file delete $_dbfile
103 # Canonise dbfile >>>
105 set db "db,$this"
106 sqlite3 [namespace current]::$db $_dbfile
108 init_db
110 set auth [$comp cget -auth]
111 $comp handler $tag [code $this req_handler]
115 body tlc::DSchan_backend::destructor {} { #<<<1
116 if {[info exists db]} {
117 $db close
118 unset db
119 if {!($persist) && $_dbfile != ":memory:"} {
120 if {[file exists $_dbname]} {
121 file delete $_dbname
128 body tlc::DSchan_backend::register_pool {pool {check_cb {}}} { #<<<1
129 set pools($pool) $check_cb
130 log debug "registered pool"
131 announce_pool $pool
135 body tlc::DSchan_backend::deregister_pool {pool} { #<<<1
136 check_pool $pool
137 if {[info exists pool_jmids($pool)]} {
138 $auth jm_can $pool_jmids($pool) [list pool_deregistered]
139 $auth chans deregister_chan $pool_jmids($pool)
141 array unset pool_jmids $pool
142 $db eval {
143 delete from
144 pool_data
145 where
146 pool = $pool
148 array unset pools $pool
149 log debug "deregistered pool"
153 body tlc::DSchan_backend::add_item {pool item} { #<<<1
154 log debug
155 check_pool $pool
156 set id [lindex $item $id_column]
158 if {[id_exists $pool $id]} {
159 error "ID \"$id\" already exists in ($pool), adding ([join $item |])" \
160 "" [list duplicate_id $id $pool]
162 set last_updated [clock seconds]
163 $db eval {
164 insert into pool_data (
165 pool,
167 last_updated,
168 data
169 ) values (
170 $pool,
171 $id,
172 $last_updated,
173 $item
177 log debug "announcing new item ($id) in pool ($pool)"
178 announce_new $pool $id $item
182 body tlc::DSchan_backend::change_item {pool id newitem} { #<<<1
183 check_pool $pool
185 if {![id_exists $pool $id]} {
186 error "ID $id does not exist in $pool"
189 set newid [lindex $newitem $id_column]
190 if {$newid != $id} {
191 error "Changing id column not allowed" "" \
192 [list id_column_changed $id $newid]
195 set olditem [get_item $pool $id]
196 if {$olditem == $newitem} return
198 set last_updated [clock seconds]
199 $db eval {
200 update
201 pool_data
203 data = $newitem,
204 last_updated = $last_updated
205 where
206 id = $id
207 and pool = $pool
209 log debug "\nannouncing item change ($id) in pool ($pool)\nold: ($olditem)\nnew: ($newitem)"
210 announce_changed $pool $id $olditem $newitem
214 body tlc::DSchan_backend::remove_item {pool id} { #<<<1
215 check_pool $pool
216 if {![id_exists $pool $id]} {
217 log warning "item ($id) not found in pool ($pool)"
218 return
221 set item [get_item $pool $id]
222 if {!($in_init_transaction)} {
223 $db eval {begin}
225 tlc::try {
226 set dbid [$db onecolumn {
227 select
228 autoid
229 from
230 pool_data
231 where
232 id = $id
233 and pool = $pool
235 $db eval {
236 delete from
237 pool_data
238 where
239 id = $id
240 and pool = $pool
242 set timeofdeath [clock seconds]
243 $db eval {
244 insert into recently_deceased (
245 dbid,
246 timeofdeath
247 ) values (
248 $dbid,
249 $timeofdeath
252 } onok {
253 if {!($in_init_transaction)} {
254 $db eval {commit}
256 } onerr {
257 default {
258 $db eval {rollback}
259 error $errmsg "" $::errorCode
262 log debug "announcing item removal ($id) from pool ($pool)"
263 announce_removed $pool $id $item
267 body tlc::DSchan_backend::get_item {pool id} { #<<<1
268 check_pool $pool
269 set rows [$db eval {
270 select
271 data
272 from
273 pool_data
274 where
275 id = $id
276 and pool = $pool
278 # WARNING: this logic will break if more columns are selected
279 if {[llength $rows] == 0} {
280 error "ID $id does not exist in $pool"
282 if {[llength $rows] > 1} {
283 log warning "Duplicate rows for id ($id) and pool ($pool)"
285 return [lindex $rows 0]
289 body tlc::DSchan_backend::item_count {pool} { #<<<1
290 check_pool $pool
291 return [$db onecolumn {
292 select
293 count(1)
294 from
295 pool_data
296 where
297 pool = $pool
302 body tlc::DSchan_backend::id_list {pool} { #<<<1
303 check_pool $pool
304 return [$db eval {
305 select
307 from
308 pool_data
309 where
310 pool = $pool
315 body tlc::DSchan_backend::announce_pool {pool} { #<<<1
316 log debug "general_info_jmid exists: [info exists general_info_jmid]"
317 if {[info exists general_info_jmid]} {
318 $auth jm $general_info_jmid [list new_pool $pool]
323 body tlc::DSchan_backend::announce_new {pool id item} { #<<<1
324 log debug "pool exists: [info exists pool_jmids($pool)]"
325 if {[info exists pool_jmids($pool)]} {
326 $auth jm $pool_jmids($pool) [list new $id $item]
331 body tlc::DSchan_backend::announce_changed {pool id olditem newitem} { #<<<1
332 log debug "pool exists: [info exists pool_jmids($pool)]"
333 if {[info exists pool_jmids($pool)]} {
334 $auth jm $pool_jmids($pool) [list changed $id $olditem $newitem]
339 body tlc::DSchan_backend::announce_removed {pool id item} { #<<<1
340 log debug "pool exists: [info exists pool_jmids($pool)]"
341 if {[info exists pool_jmids($pool)]} {
342 $auth jm $pool_jmids($pool) [list removed $id $item]
347 body tlc::DSchan_backend::check_pool {pool} { #<<<1
348 if {![info exists pools($pool)]} {
349 return -code error "No such pool: ($pool)"
354 body tlc::DSchan_backend::req_handler {auth user seq rest} { #<<<1
355 switch -- [lindex $rest 0] {
356 "setup_chans" {
357 set extra [lindex $rest 1]
359 set userpools {}
360 foreach {pool check_cb} [array get pools] {
361 if {[catch {
362 # Provide a place for the check_cb callback to scribble into
363 # via an upvar command. We send the contents to the client
364 array unset pool_meta
365 array set pool_meta {}
367 log debug "check_cb is: ($check_cb)"
368 if {
369 $check_cb == {}
370 || [eval $check_cb [list $user $pool $extra]]
372 lappend userpools $pool
375 set pool_meta_all($pool) [array get pool_meta]
376 log debug "Saving pool_meta for ($pool):\n$pool_meta_all($pool)"
377 } errmsg]} {
378 log error "error calling check_cb:\n$::errorInfo"
382 if {![info exists general_info_jmid]} {
383 set general_info_jmid [$auth unique_id]
384 $auth chans register_chan $general_info_jmid \
385 [code $this general_info_chan_cb]
387 $auth pr_jm $general_info_jmid $seq [list general [list \
388 headers $headers \
389 id_column $id_column \
392 foreach pool $userpools {
393 if {![info exists pool_jmids($pool)]} {
394 set pool_jmids($pool) [$auth unique_id]
395 $auth chans register_chan $pool_jmids($pool) \
396 [code $this pool_chan_cb $pool]
399 log debug "Contents of pool_meta array:\n[array get pool_meta]"
400 set all_items [$db eval {
401 select
402 data
403 from
404 pool_data
405 where
406 pool = $pool
408 $auth pr_jm $pool_jmids($pool) $seq [list datachan $pool $all_items $pool_meta_all($pool)]
411 $auth ack $seq ""
414 "setup_new_pool" {
415 set new_pool [lindex $rest 1]
416 set extra [lindex $rest 2]
418 if {![info exists pools($new_pool)]} {
419 $auth nack $seq "No such pool: ($new_pool)"
420 return
422 set check_cb $pools($new_pool)
423 if {[catch {
425 # Provide a place for the check_cb callback to scribble into
426 # via an upvar command. We send the contents to the client
427 array set pool_meta {}
429 if {
430 $check_cb == {}
431 || [eval $check_cb [list $user $new_pool $extra]]
433 if {![info exists pool_jmids($new_pool)]} {
434 set pool_jmids($new_pool) [$auth unique_id]
435 $auth chans register_chan $pool_jmids($new_pool) \
436 [code $this pool_chan_cb $new_pool]
439 set all_items [$db eval {
440 select
441 data
442 from
443 pool_data
444 where
445 pool = $new_pool
447 $auth pr_jm $pool_jmids($new_pool) $seq [list datachan $new_pool $all_items [array get pool_meta]]
448 log debug "Added user ([$user name]) to new pool ($new_pool), with ([llength $all_items]) initial items"
450 $auth ack $seq ""
451 } else {
452 log debug "User ([$user name]) is not a viewer of pool ($new_pool)"
453 $auth ack $seq ""
455 } errmsg]} {
456 log error "error calling check_cb:\n$::errorInfo"
457 $auth nack $seq "Internal error"
458 return
462 default {
463 log error "invalid req type: [lindex $rest 0]"
464 $auth nack $seq "Invalid req type: ([lindex $rest 0])"
470 body tlc::DSchan_backend::pool_chan_cb {pool op data} { #<<<1
471 switch -- $op {
472 cancelled {
473 log debug "all destinations disconnected"
474 array unset pool_jmids $pool
477 req {
478 foreach {seq prev_seq msg} $data break
479 $auth nack $seq "Requests not allowed on this channel"
482 default {
483 log error "unexpected op: ($op)"
489 body tlc::DSchan_backend::general_info_chan_cb {op data} { #<<<1
490 switch -- $op {
491 cancelled {
492 log debug "all destinations disconnected"
493 if {[info exists general_info_jmid]} {
494 unset general_info_jmid
498 req {
499 foreach {seq prev_seq msg} $data break
500 $auth nack $seq "Requests not allowed on this channel"
503 default {
504 log error "unexpected op: ($op)"
510 body tlc::DSchan_backend::pool_exists {pool} { #<<<1
511 return [info exists pools($pool)]
515 body tlc::DSchan_backend::init_db {} { #<<<1
516 set exists [$db onecolumn {
517 select
518 count(1) > 0
519 from
520 sqlite_master
521 where
522 type = 'table'
523 and name = 'pool_data'
526 if {!($persist) && $exists} {
527 $db eval {
528 drop table pool_data;
529 drop table recently_deceased;
531 set exists 0
534 # recently_deceased.dbid == -1 gives the last cleanout time
535 if {!($exists)} {
536 $db eval {
537 create table pool_data (
538 autoid integer primary key autoincrement,
539 pool text,
540 id text,
541 last_updated integer,
542 data text
544 create index pool_data_pool_idx on pool_data(pool);
545 create index pool_data_id_idx on pool_data(id);
547 create table recently_deceased (
548 dbid integer not null,
549 timeofdeath integer not null
551 create index recently_deceased_timeofdeath_idx
552 on recently_deceased(timeofdeath);
558 body tlc::DSchan_backend::id_exists {pool id} { #<<<1
559 return [$db onecolumn {
560 select
561 count(1) > 0
562 from
563 pool_data
564 where
565 pool = $pool
566 and id = $id
571 body tlc::DSchan_backend::start_init {} { #<<<1
572 $db eval {begin}
573 set in_init_transaction 1
577 body tlc::DSchan_backend::end_init {} { #<<<1
578 $db eval {commit; analyze}
579 set in_init_transaction 0
583 body tlc::DSchan_backend::abort_init {} { #<<<1
584 $db eval {rollback}
585 set in_init_transaction 0