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
|
#!/usr/bin/tclsh9.0
#
#
namespace eval ::syslogtest::server {
variable sourceChannel ""
variable sourceKind ""
variable sourceCmd {}
variable listener ""
variable running 1
}
proc ::syslogtest::server::usage {} {
puts stderr "usage: server.tcl ?-source syslog|journalctl?"
exit 2
}
proc ::syslogtest::server::parseArgs {argv} {
set opts [dict create -source auto]
set n [llength $argv]
for {set i 0} {$i < $n} {incr i} {
set arg [lindex $argv $i]
switch -- $arg {
-port -
-source {
incr i
if {$i >= $n} {
[namespace current]::usage
}
dict set opts $arg [lindex $argv $i]
}
default {
[namespace current]::usage
}
}
}
return $opts
}
proc ::syslogtest::server::buildSourceCommand {preferred} {
set tail [auto_execok tail]
set journalctl [auto_execok journalctl]
if {$preferred eq "syslog"} {
if {[file readable /var/log/syslog] && $tail ne ""} {
return [dict create kind syslog cmd [list $tail -n 10 -f /var/log/syslog]]
}
error "requested syslog source is unavailable"
}
if {$preferred eq "journalctl"} {
if {$journalctl ne ""} {
return [dict create kind journalctl cmd [list $journalctl -f -n 10 --no-pager -o short-iso]]
}
error "requested journalctl source is unavailable"
}
# this is the default behaviour which is determined in parseArgs (-source auto)
if {[file readable /var/log/syslog] && $tail ne ""} {
return [dict create kind syslog cmd [list $tail -n 10 -f /var/log/syslog]]
}
if {$journalctl ne ""} {
return [dict create kind journalctl cmd [list $journalctl -f -n 0 --no-pager -o short-iso]]
}
# if we get here there is nothing we can do
error "no syslog source available (need readable /var/log/syslog or journalctl)"
}
proc ::syslogtest::server::openSource {} {
variable sourceChannel
variable sourceKind
variable sourceCmd
if {$sourceChannel ne ""} { catch {close $sourceChannel} }
puts "opening source with command --> $sourceCmd"
set sourceChannel [open |$sourceCmd r]
fconfigure $sourceChannel -blocking 0 -buffering line -encoding utf-8 -translation auto
}
proc ::syslogtest::server::parseLine {line} {
set tsKind none
set payload $line
if {[regexp {^([A-Z][a-z]{2}\s+[ 0-9][0-9]\s[0-9]{2}:[0-9]{2}:[0-9]{2})\s+\S+\s+(.*)$} $line -> _ rest]} {
set tsKind rfc3164
set payload $rest
} elseif {[regexp {^([0-9]{4}-[0-9]{2}-[0-9]{2}[T ][0-9:.+-]+)\s+\S+\s+(.*)$} $line -> _ rest]} {
set tsKind iso8601
set payload $rest
} elseif {[regexp {^[A-Z][a-z]{2}\s+[ 0-9][0-9]\s[0-9]{2}:[0-9]{2}:[0-9]{2}\s+(.*)$} $line -> rest]} {
set tsKind rfc3164
set payload $rest
} elseif {[regexp {^[0-9]{4}-[0-9]{2}-[0-9]{2}[T ][0-9:.+-]+\s+(.*)$} $line -> rest]} {
set tsKind iso8601
set payload $rest
}
if {[regexp {^[^:]+:\s*(.*)$} $payload -> msg]} {
set payload $msg
}
return [dict create raw $line payload $payload timestamp_kind $tsKind]
}
proc ::syslogtest::server::matches {mode pattern parsed} {
set payload [dict get $parsed payload]
switch -- $mode {
literal {
return [expr {[string first $pattern $payload] >= 0}]
}
regexp {
return [regexp -- $pattern $payload]
}
default {
error "unsupported mode '$mode'"
}
}
}
proc ::syslogtest::server::waitFor {mode pattern timeoutMs} {
variable sourceChannel
if {![string is integer -strict $timeoutMs] || $timeoutMs <= 0} {
error "timeout must be a positive integer in milliseconds"
}
set deadline [expr {[clock milliseconds] + $timeoutMs}]
while {[clock milliseconds] <= $deadline} {
if {[eof $sourceChannel]} {
[namespace current]::openSource
after 50
continue
}
set n [gets $sourceChannel line]
if {$n >= 0} {
set parsed_d [parseLine $line]
if {[[namespace current]::matches $mode $pattern $parsed_d]} {
return $parsed_d
}
continue
}
after 50
}
error "timeout after ${timeoutMs}ms waiting for $mode match"
}
proc ::syslogtest::server::writeResponse {chan status args} {
puts $chan [list $status {*}$args]
flush $chan
}
proc ::syslogtest::server::onClientReadable {chan} {
variable running
if {[eof $chan]} {
catch {
chan event $chan readable ""
close $chan
}
puts "---> "
return
}
set n [gets $chan line]
if {$n < 0} { return }
if {$line eq ""} {
[namespace current]::writeResponse $chan error "empty request"
return
}
puts "<--- $line"
set cmd [lindex $line 0]
switch -- $cmd {
ping {
[namespace current]::writeResponse $chan ok pong
}
wait {
if {[llength $line] != 4} {
[namespace current]::writeResponse $chan error "usage: wait literal|regexp pattern timeoutMs"
return
}
set mode [lindex $line 1]
set pattern [lindex $line 2]
set timeoutMs [lindex $line 3]
if {[catch {set parsed [[namespace current]::waitFor $mode $pattern $timeoutMs]} err]} {
[namespace current]::writeResponse $chan error $err
return
}
[namespace current]::writeResponse $chan ok \
[dict get $parsed raw] \
[dict get $parsed payload] \
[dict get $parsed timestamp_kind]
}
shutdown {
[namespace current]::writeResponse $chan ok bye
set running 0
}
default {
[namespace current]::writeResponse $chan error "unknown command '$cmd'"
}
}
}
proc ::syslogtest::server::acceptClient {chan _addr _port} {
chan configure $chan -blocking 0 -buffering line -encoding utf-8 -translation lf
chan event $chan readable [list ::syslogtest::server::onClientReadable $chan]
}
proc ::syslogtest::server::main {argv} {
variable sourceCmd
variable sourceKind
variable listener
variable running
set opts [parseArgs $argv]
set source [buildSourceCommand [dict get $opts -source]]
set sourceCmd [dict get $source cmd]
set sourceKind [dict get $source kind]
[namespace current]::openSource
if {![dict exists $opts -port]} { [namespace current]::usage }
set port [dict get $opts -port]
set listener [socket -server ::syslogtest::server::acceptClient -myaddr 127.0.0.1 $port]
puts "server started with pid [pid]"
while {$running} {
vwait ::syslogtest::server::running
}
catch {close $listener}
catch {close $::syslogtest::server::sourceChannel}
catch {file delete -force -- $portfile}
}
if {[catch {::syslogtest::server::main $argv} err opts]} {
puts stderr "syslog test server error: $err"
if {[dict exists $opts -errorinfo]} {
puts stderr [dict get $opts -errorinfo]
}
exit 1
}
|