X-Git-Url: https://www.chiark.greenend.org.uk/ucgi/~mdw/git/ircbot/blobdiff_plain/6b33d29a2ffb4244bae6b51b2114039e2edb54ba..22368e4c2039ee2d245a9322ff9a5b68f3dc38d0:/ledmodule.tcl diff --git a/ledmodule.tcl b/ledmodule.tcl index 90bc013..250f600 100644 --- a/ledmodule.tcl +++ b/ledmodule.tcl @@ -37,29 +37,57 @@ set helpfile ledhelp source irccore.tcl source parsecmd.tcl source stdhelp.tcl +source userv.tcl + +defset errchan #$nick +defset retry_after 900000 +defset chan_after 3000 +defset chans_retry 3600000 +defset debug_reset_after 86400000 + +defset debugusers {} # variables # # monitor/$monname(chans) -> [list $chan1 $chan2 ...] # monitor/$monname(ignore) -> [list $regexp ...] # monitor/$monname(prefer) -> [list $regexp ...] -# monitor/$monname(present) -> [list $lnick ...] +# monitor/$monname(present-$chan) -> [list $lnick ...] # monitor/$monname(last-talk) -> $time_t # monitor/$monname(last-talkpref) -> $time_t # monitor/$monname(time-recent) -> $seconds # monitor/$monname(time-recentnow) -> $seconds +# monitor/$monname(talkchange) -> [after ...] or unset # -# deviceset/$username:$lno(monname) -> $monname -# deviceset/$username:$lno(group) -> $led_group -# deviceset/$username:$lno(states) -> [list $state1 $value1 $state2 ...] -# deviceset/$username:$lno(fchan) -> [open remoteleds ... |] or unset -# deviceset/$username:$lno(retry) -> [after ... ] or unset +# deviceset/$username:$lno(monname) -> $monname +# deviceset/$username:$lno(group) -> $led_group +# deviceset/$username:$lno(username) -> $username +# deviceset/$username:$lno(values) -> $valuestring +# deviceset/$username:$lno(states) -> [list $state1 $value1 $state2 ...] +# deviceset/$username:$lno(ochan) -> [open remoteleds ... | r] or unset +# deviceset/$username:$lno(ichan) -> fifo for remoteleds input or unset +# deviceset/$username:$lno(retry) -> [after ... ] or unset # # onchans($chan) [list mustleave] # in config_chane # onchans($chan) [list idle] # onchans($chan) [list forced] # for errchan # onchans($chan) [list shortly [after ...]] # do a NAMES +proc ldebug {facil m} { + global debugusers + # facil is + # m$monname + # d$deviceset + # c$lchan + # {} for system stuff + if {![llength debugusers]} return + if {[regexp {[md]([^:]+)\:} $facil dummy username] && + [lsearch -exact $debugusers $username]==-1} return + + regsub {^(.)} $facil {\1 } cc + reporterr "DEBUG $cc $m" +} + proc list_objs {vp} { set l {} foreach v [info globals] { @@ -69,16 +97,18 @@ proc list_objs {vp} { return $l } -proc privmsg_unlogged {prefix ischan params} { - if {!$ischan} { - prefix_nick - execute_usercommand $p PRIVMSG $n $n \ - [lindex $params 0] [lindex $params 1] - return 0 - } +proc privmsg_unlogged {p ischan params} { + global errorInfo + if {!$ischan} { return 0 } - foreach m [list_objs monitor] { - mon_speech $m [irctolower [lindex $params 0]] [irctolower $n] + # on-channel message + if {[catch { + prefix_nick + foreach m [list_objs monitor] { + mon_speech $m [irctolower [lindex $params 0]] [irctolower $n] + } + } emsg]} { + log "processing error: $emsg\n$errorInfo" } return 1; } @@ -88,100 +118,174 @@ proc reporterr {m} { sendprivmsg $errchan $m } +proc msg_PRIVMSG {p c dest text} { + global errchan + prefix_nick + execute_usercommand $p $c $n $errchan $dest $text +} + proc proc_mon {name argl body} { proc mon_$name [concat m $argl] " upvar #0 monitor/\$m mm $body" } +proc mon_nick_is {globlist ln} { + foreach gl $globlist { + if {[string match $gl $ln]} { return 1 } + } + return 0 +} + +proc_mon gotchanlist {ch nll} { + if {[lsearch -exact $mm(chans) $ch] == -1} return + set mm(present-$ch) $nll + mon_updateall $m +} + proc_mon speech {chan ln} { - if {[search -exact $mm(chans) $chan] == -1} return + if {[lsearch -exact $mm(chans) $chan] == -1} return if {[mon_nick_is $mm(ignore) $ln]} return set now [clock seconds] set mm(last-talk) $now - if {[mon_nick_is $mm(prefer)]} { set mm(last-talkpref) $now } + if {[mon_nick_is $mm(prefer) $ln]} { set mm(last-talkpref) $now } mon_updateall $m } proc_mon calcstate {} { - set s " " - if {[llength $mm(present)]} { append s "present " } + set s " default " + foreach ch $mm(chans) { + if {[llength $mm(present-$ch)]} { append s "present "; break } + } set now [clock seconds] - for p {{} pref} { + set valid_until [expr {$now + 86400}] + set refresh_later 0 + catch { after cancel $mm(talkchange) } + foreach p {{} pref} { foreach t {{} now} { - set since [expr {$now - $mm(time-recent$t)}] - if {[expr {$mm(last-talk$pref) < $since}]} continue + set vu [expr {$mm(last-talk$p) + $mm(time-recent$t)}] + if {$vu < $now} continue append s "${p}talk${t} " + set refresh_later 1 + if {$vu < $valid_until} { set valid_until $vu } } } + regsub {^ default } $s { } ss + set ds [string trim $ss] + if {$refresh_later} { + set interval [expr {$valid_until - $now + 2}] + set ivms [expr {$interval*1000}] + set mm(talkchange) [after $ivms [list mon_updateall $m]] + ldebug m$m "until now+${interval}: $ds" + } else { + ldebug m$m "indefinitely: $ds" + } return $s } proc_mon updateall {} { set s [mon_calcstate $m] - for d [list_objs deviceset] { + foreach d [list_objs deviceset] { upvar #0 deviceset/$d dd if {[string compare $m $dd(monname)]} continue - dset_setbystate $s + dset_setbystate $d $s } } proc_mon destroy {} { + ldebug m$m "destroying" + catch { after cancel $mm(talkchange) } catch { unset mm } } proc proc_dset {name argl body} { proc dset_$name [concat d $argl] " upvar #0 deviceset/\$d dd - if {[catch { + if {\[catch { $body - } emsg]} { + } emsg\]==1} { reporterr \"error on \$d: \$emsg\" }" } +proc timed_log {m} { + log "[clock seconds] $m" +} + proc_dset setbystate {s} { - set lv {} - foreach {sq v} { + foreach {sq v} $dd(states) { if {![string match *$sq* $s]} continue set lv $v; break } - puts $dd(fchan) $lv + if {![info exists lv]} { + reporterr "no state for $d matching$s" + return + } + ldebug d$d "matches $sq: $v" + timed_log "->$d $lv" + set dd(values) "$sq=$lv" + puts $dd(ichan) $lv } -proc dset_destroy {} { +proc_dset destroy {} { + ldebug d$d "destroying" catch { after cancel $dd(retry) } - catch { close $dd(fchan) } + catch { + if {[info exists dd(ochan)]} { timed_log ">\$$d destroy" } + close $dd(ochan) + close $dd(ichan) + } catch { unset dd } } +proc modvar_save_copy {cv defv} { + upvar 1 m m + upvar 1 mm mm + upvar 1 save/$m save + if {[info exists save($cv)]} { + set mm($cv) $save($cv) + } else { + set mm($cv) $defv + } +} + proc reloaduser {username} { check_username $username + ldebug u$username "reloading" if {[catch { set cfg [exec userv --timeout 3 $username irc-ledcontrol-config \ < /dev/null] - set pw [exec userv --timeout 3 $username irc-ledcontrol-passwords \ - < /dev/null > pwdb/p$username] } emsg]} { + regsub "\n" $emsg " // " emsg reporterr "error reloading $username: $emsg" + return "" } - for d [list_objs deviceset] { + foreach d [list_objs deviceset] { if {![string match $username:* $d]} continue dset_destroy $d } - for m [list_objs monitor] { + foreach m [list_objs monitor] { if {![string match $username* $m]} continue - mon_destroy $m + upvar #0 monitor/$m mm + foreach cv [array names mm] { set save/${m}($cv) $mm($cv) } } if {![string length $cfg]} { - file remove pwdb/$username + file delete pwdb/$username + return "no config from $username" } elseif {[catch { + exec userv --timeout 3 $username irc-ledcontrol-passwords \ + < /dev/null > pwdb/p$username + } emsg]} { + reporterr "error reading passwords for $username: $emsg" + return "" + } elseif {[catch { + ldebug u$username "parsing" foreach cv {ignore nopresence prefer} { set cc($cv) {} } set cc(time-recentnow) 120 set cc(time-recent) 450 - set pline 0 + set lno 0 foreach l [split $cfg "\n"] { - incr pline + incr lno set l [string trim $l] if {[regexp {^\#} $l]} { } elseif {[regexp {^nick\s+(ignore|nopresence|prefer)\s+(\S.*)$} \ @@ -199,25 +303,34 @@ proc reloaduser {username} { if {![string match $username:* $m]} { error "monname must start with $username:" } + check_monname $m foreach ch [split $cl " "] { if {![string length $ch]} continue - check_nick $ch + check_chan $ch if {![ischan $ch]} { error "invalid channel $ch" } lappend cc(chans) [irctolower $ch] + chan_shortly $ch } upvar #0 monitor/$m mm foreach cv [array names cc] { set mm($cv) $cc($cv) } - foreach cv {{} pref} { set mm(last-talk$cv) 0 } + foreach cv {{} pref} { + modvar_save_copy last-talk$cv 0 + } + foreach cv [array names mm(chans)] { + modvar_save_copy present-$cv {} + } + ldebug m$m "created" } elseif {[regexp \ {^leds\s+([0-9A-Za-z][-:/0-9A-Za-z]+)\s+(\S+)\s+(\S+.*)$} \ $l dummy g m states]} { set d $username:$lno:$g set sl {} + check_monname $m foreach sv [split $states " "] { if {![string length $sv]} continue if {![regexp \ - {^((pref)?talk(now)?|present|default)\=([0-9a-z][,/+0-9A-Za-z]*)$} \ - $sv dummy lhs dummy dummy rhs]} { + {^((?:pref)?talk(?:now)?|present|default)\=([0-9a-z][,/+0-9A-Za-z]*)$} \ + $sv dummy lhs rhs]} { error "invalid state spec" } lappend sl $lhs $rhs @@ -226,47 +339,79 @@ proc reloaduser {username} { set dd(monname) $m set dd(states) $sl set dd(group) $g + set dd(values) startup + set dd(username) $username dset_start $d + ldebug d$d "created" } } } emsg]} { - reporterr "setup error $username:$pline:$emsg" + reporterr "setup error $username:$lno:$emsg" + return "" + } else { + return "reloaded $username" + } +} + +proc check_monname {m} { + if {[regexp {[^-_+:.#0-9a-zA-Z]} $m badchar]} { + error "char $badchar not allowed in monnames" + } + if {![regexp {^[0-9a-zA-Z]} $m]} { + error "monname must start with alphanum" } } proc_dset start {} { catch { unset dd(retry) } + set username $dd(username) + ldebug d$d "starting" if {[catch { - set fchan [open [list | \ - remoteleds 2>&1 --pipe $g \ - --passfile-only pwdb/p$username \ - |& cat \ - ]] - fconfigure $fchan -blocking 0 - fileevent $fchan readable [list dset_rledout $d] - set dd(fchan) $fchan + set cmdl [list remoteleds --pipe $dd(group) \ + --human --passfile-only pwdb/p$username] + timed_log "!-$d [join $cmdl " "]" + lappend cmdl < pwdb/fifo |& cat + catch { file delete pwdb/fifo } + exec mkfifo -m 0600 pwdb/fifo + set ichan [open pwdb/fifo r+] + set ochan [open |$cmdl r] + fconfigure $ichan -blocking 0 -buffering line + fconfigure $ochan -blocking 0 -buffering line + fileevent $ochan readable [list dset_rledout $d] + set dd(ichan) $ichan + set dd(ochan) $ochan } emsg]} { reporterr "remoteleds startup $d: $emsg" + catch { close $ichan } + catch { close $ochan } dset_trylater $d } } proc_dset rledout {} { - global errchan retry_after - while {[gets $dd(fchan) l] != -1} { reporterr "remoteleds on $d: $l" } - if {[fblocked $dd(fchan)]} return - catch { close $dd(fchan) } - unset dd(fchan) + global errchan + while {[gets $dd(ochan) l] != -1} { + reporterr "remoteleds on $d: $dd(values): $l" + } + if {[fblocked $dd(ochan)]} return + timed_log ">\$$d failure"; + catch { close $dd(ichan) } + catch { close $dd(ochan) } + unset dd(ichan) + unset dd(ochan) reporterr "remoteleds on $d died" dset_trylater $d } proc_dset trylater {} { - set dd(retry) [after $retry_after [list proc_dset start $d]] + global retry_after + ldebug d$d "will try again later" + set dd(retry) [after $retry_after [list dset_start $d]] } proc config_change {} { - global onchans chans_retry errchan + global onchans chans_retry errchan config_retry_after + ldebug {} "rechecking configuration etc" foreach ch [array names onchans] { manyset $onchans($ch) status after if {"$status" == "shortly"} { @@ -274,8 +419,8 @@ proc config_change {} { } set onchans($ch) mustleave } - set ch($errchan) forced sendout JOIN $errchan + chan_shortly $errchan foreach m [list_objs monitor] { upvar #0 monitor/$m mm foreach ch $mm(chans) { @@ -288,40 +433,104 @@ proc config_change {} { sendout PART $ch unset onchans($ch) } - after $chans_retry config_change + catch { after cancel $config_retry_after } + set config_retry_after [after $chans_retry config_change] } proc chan_shortly {ch} { global chan_after upvar #0 onchans($ch) oc - manyset $oc status after - if {"$status" != "idle"} return + if {[info exists oc]} { + manyset $oc status after + if {"$status" == "shortly"} { + ldebug c$ch "queued check already pending" + return + } + } + ldebug c$ch "queueing check" set oc [list shortly [after $chan_after chan_sendnames $ch]] } +proc msg_353 {p c dest type chan nicklist} { + set lchan [irctolower $chan] + set nll [irctolower $nicklist] + ldebug c$lchan "got names $nll" + foreach m [list_objs monitor] { + mon_gotchanlist $m $lchan $nll + } +} + proc chan_sendnames {ch} { upvar #0 onchans($ch) oc + ldebug c$ch "asking for namelist" sendout NAMES $ch set oc idle } -def_ucmd reload {} { +def_ucmd reload { set username [ta_word] ta_nomore - reloaduser $username + set m [reloaduser $username] config_change + ucmdr {} $m +} + +proc debug_reset {} { + global debugusers debug_cancelling + unset debug_cancelling + set debugusers {} + reporterr "debug mode timed out" +} + +def_ucmd debug { + prefix_nick + global debugusers debug_cancelling debug_reset_after + if {![string length $text]} { error "must give list of usernames" } + llength $text + set debugusers $text + catch { after cancel $debug_cancelling } + set debug_cancelling [after $debug_reset_after debug_reset] + reporterr "debug enabled by $n: $debugusers" +} + +def_ucmd nodebug { + prefix_nick + global debugusers debug_cancelling + set debugusers {} + catch { after cancel $debug_cancelling } + catch { unset debug_cancelling } + reporterr "debug disabled by $n" +} + +def_ucmd who { + set r {} + foreach m [list_objs monitor] { + upvar #0 monitor/$m mm + lappend r "monitoring $mm(chans) for $m" + } + foreach d [list_objs deviceset] { + upvar #0 deviceset/$d dd + regexp {^[^:]*\:[^:]*} $dd(group) dest + lappend r "sending $dd(monname) to $dest" + } + ucmdr [join $r "\n"] {} } -proc connected { +proc connected {} { + ldebug {} "connected" foreach f [glob -nocomplain pwdb/p*] { regexp {^pwdb/p(.*)$} $f dummy username - check_username $username - reloaduser $username + set m [reloaduser $username] } config_change } -# fixme -# 353 # JOIN PART # KICK KILL QUIT + +if {[catch { + loadhelp + ensure_connecting +} emsg]} { + fail "startup: $emsg" +}