Multiple changes.
- Use the ARC memory model for release builds. - Move logfile to a command line switch, to avoid chicken-and-egg logging when failing to parse YAML. - Fix logic bug: Stop processing rules on a good match - Add performance timer and memory used when logging to file - Rename 'headers' to 'match' in configuration file, more intention revealing - Add logger explicit flock (unlock) at process exit FossilOrigin-Name: 7c439d99044b8d725c2dc1a806eec14fff7f0675afb14920de3c7c2581907640
This commit is contained in:
parent
5b2e0b52bc
commit
a938cf045a
7 changed files with 110 additions and 59 deletions
4
Makefile
4
Makefile
|
|
@ -11,7 +11,7 @@ dependencies:
|
||||||
|
|
||||||
development: ${FILES}
|
development: ${FILES}
|
||||||
# can use gdb with this...
|
# can use gdb with this...
|
||||||
nim --verbosity:2 --debugInfo --assertions:on --linedir:on -d:debug -d:nimTypeNames --nimcache:.cache c ${FILES}
|
nim --verbosity:2 --debugInfo --assertions:on --stacktrace:on --linedir:on -d:debug -d:nimTypeNames --nimcache:.cache c ${FILES}
|
||||||
mv src/sieb .
|
mv src/sieb .
|
||||||
|
|
||||||
debugger: ${FILES}
|
debugger: ${FILES}
|
||||||
|
|
@ -19,7 +19,7 @@ debugger: ${FILES}
|
||||||
mv src/sieb .
|
mv src/sieb .
|
||||||
|
|
||||||
release:dependencies ${FILES}
|
release:dependencies ${FILES}
|
||||||
nim -d:release -d:strip --passc:-flto --opt:speed --nimcache:.cache c ${FILES}
|
nim -d:release -d:strip --mm:arc -d:lto --opt:speed --nimcache:.cache c ${FILES}
|
||||||
mv src/sieb .
|
mv src/sieb .
|
||||||
|
|
||||||
docs:
|
docs:
|
||||||
|
|
|
||||||
25
config.yml
25
config.yml
|
|
@ -2,15 +2,13 @@
|
||||||
# Example Sieb configuration file.
|
# Example Sieb configuration file.
|
||||||
#
|
#
|
||||||
|
|
||||||
# Default, no logging. Relative to homedir.
|
|
||||||
logfile: sieb.log
|
|
||||||
|
|
||||||
## Rules tried before global filtering
|
# Rules tried before global filtering.
|
||||||
|
#
|
||||||
early_rules:
|
early_rules:
|
||||||
-
|
-
|
||||||
headers:
|
match:
|
||||||
TO: mhlon@(laika|ravn)
|
TO: ahlon@(laika|ravn|martini)
|
||||||
received: .*sendgrid.*
|
|
||||||
filter:
|
filter:
|
||||||
- [ reformail, -A, "X-Sieb: This matched." ]
|
- [ reformail, -A, "X-Sieb: This matched." ]
|
||||||
deliver: .whatever
|
deliver: .whatever
|
||||||
|
|
@ -27,9 +25,18 @@ filter:
|
||||||
##
|
##
|
||||||
rules:
|
rules:
|
||||||
-
|
-
|
||||||
headers:
|
match:
|
||||||
x-sieb: global
|
x-one: global
|
||||||
deliver: .whoas
|
-
|
||||||
|
match:
|
||||||
|
Subject: .*\s+corepacket\s+.*
|
||||||
|
deliver: .balls
|
||||||
|
filter:
|
||||||
|
- [ reformail, -A, "X-Sieb: Boom!" ]
|
||||||
|
-
|
||||||
|
match:
|
||||||
|
x-three: global
|
||||||
|
|
||||||
|
|
||||||
# # Magic "TO" which means To: OR Cc:
|
# # Magic "TO" which means To: OR Cc:
|
||||||
# -
|
# -
|
||||||
|
|
|
||||||
|
|
@ -11,7 +11,6 @@
|
||||||
import
|
import
|
||||||
std/os,
|
std/os,
|
||||||
std/streams,
|
std/streams,
|
||||||
std/strformat,
|
|
||||||
std/tables,
|
std/tables,
|
||||||
yaml/parser,
|
yaml/parser,
|
||||||
yaml/serialization
|
yaml/serialization
|
||||||
|
|
@ -35,13 +34,12 @@ const CONFFILES = @[
|
||||||
|
|
||||||
type
|
type
|
||||||
Rule* = object
|
Rule* = object
|
||||||
headers* {.defaultVal: initTable[string, string]()}: Table[ string, string ]
|
match* {.defaultVal: initTable[string, string]()}: Table[ string, string ]
|
||||||
deliver* {.defaultVal: ""}: string
|
deliver* {.defaultVal: ""}: string
|
||||||
filter* {.defaultVal: @[]}: seq[ seq[string] ]
|
filter* {.defaultVal: @[]}: seq[ seq[string] ]
|
||||||
|
|
||||||
# Typed configuration file layout for YAML loading.
|
# Typed configuration file layout for YAML loading.
|
||||||
Config* = object
|
Config* = object
|
||||||
logfile* {.defaultVal: "".}: string
|
|
||||||
filter* {.defaultVal: @[]}: seq[ seq[string] ]
|
filter* {.defaultVal: @[]}: seq[ seq[string] ]
|
||||||
early_rules* {.defaultVal: @[]}: seq[Rule]
|
early_rules* {.defaultVal: @[]}: seq[Rule]
|
||||||
rules* {.defaultVal: @[]}: seq[Rule]
|
rules* {.defaultVal: @[]}: seq[Rule]
|
||||||
|
|
@ -53,7 +51,7 @@ type
|
||||||
|
|
||||||
proc parse( path: string ): Config =
|
proc parse( path: string ): Config =
|
||||||
## Return a parsed configuration from yaml.
|
## Return a parsed configuration from yaml.
|
||||||
debug "Using configuration at: {path}".fmt
|
"Using configuration at: $#".debug( path )
|
||||||
let stream = newFileStream( path )
|
let stream = newFileStream( path )
|
||||||
try:
|
try:
|
||||||
stream.load( result )
|
stream.load( result )
|
||||||
|
|
@ -61,18 +59,18 @@ proc parse( path: string ): Config =
|
||||||
debug err.msg
|
debug err.msg
|
||||||
return Config() # return empty default, it could be "half parsed"
|
return Config() # return empty default, it could be "half parsed"
|
||||||
except YamlConstructionError as err:
|
except YamlConstructionError as err:
|
||||||
debug err.msg
|
err.msg.debug
|
||||||
return Config()
|
return Config()
|
||||||
finally:
|
finally:
|
||||||
stream.close
|
stream.close
|
||||||
|
|
||||||
|
|
||||||
proc get_config*( path: string ): Config =
|
proc getConfig*( path: string ): Config =
|
||||||
## Choose a configuration file for parsing, or if there are
|
## Choose a configuration file for parsing, or if there are
|
||||||
## none available, return an empty config.
|
## none available, return an empty config.
|
||||||
if path != "":
|
if path != "":
|
||||||
if not path.fileExists:
|
if not path.fileExists:
|
||||||
debug "Configfile \"{path}\" unreadable, ignoring.".fmt
|
"Configfile \"$#\" unreadable, ignoring.".debug( path )
|
||||||
return
|
return
|
||||||
return parse( path )
|
return parse( path )
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -8,6 +8,8 @@
|
||||||
#############################################################
|
#############################################################
|
||||||
|
|
||||||
import
|
import
|
||||||
|
std/math,
|
||||||
|
std/monotimes,
|
||||||
std/os,
|
std/os,
|
||||||
std/posix,
|
std/posix,
|
||||||
std/times
|
std/times
|
||||||
|
|
@ -19,6 +21,7 @@ import
|
||||||
|
|
||||||
type Logger = object
|
type Logger = object
|
||||||
fh: File
|
fh: File
|
||||||
|
start: MonoTime
|
||||||
|
|
||||||
|
|
||||||
#############################################################
|
#############################################################
|
||||||
|
|
@ -32,21 +35,24 @@ var logger*: Logger
|
||||||
# M E T H O D S
|
# M E T H O D S
|
||||||
#############################################################
|
#############################################################
|
||||||
|
|
||||||
proc createLogger*( path: string ): void =
|
proc createLogger*( parentdir: string, filename: string ): void =
|
||||||
## Get in line to open a write lock to the configured logfile at +path+.
|
## Get in line to open a write lock to the configured logfile at +path+.
|
||||||
## This will block until it can get an exclusive lock.
|
## This will block until it can get an exclusive lock.
|
||||||
let path = joinPath( getHomeDir(), path )
|
let path = joinPath( parentdir, filename )
|
||||||
logger = Logger()
|
logger = Logger()
|
||||||
logger.fh = path.open( fmAppend )
|
logger.fh = path.open( fmAppend )
|
||||||
|
logger.start = getMonoTime()
|
||||||
|
|
||||||
# Wait for exclusive lock.
|
# Wait for exclusive lock.
|
||||||
discard logger.fh.getFileHandle.lockf( F_LOCK, 0 )
|
discard logger.fh.getFileHandle.lockf( F_LOCK, 0 )
|
||||||
logger.fh.writeLine "\n-------------------------------------------------------------------"
|
logger.fh.writeLine "\n", now().utc, " ------------------------------------------------------------------"
|
||||||
logger.fh.writeLine now().utc
|
|
||||||
|
|
||||||
|
|
||||||
proc close*( l: Logger ): void =
|
proc close*( l: Logger ): void =
|
||||||
## Release the lock and close/flush the file.
|
## Release the lock and close/flush the file.
|
||||||
|
let duration = float( (getMonoTime() - l.start).inNanoSeconds ) / 1_000_000 # ms
|
||||||
|
let memUsed = ( getOccupiedMem() / 1024 ).round( 2 )
|
||||||
|
l.fh.writeLine "Completed in ", duration.round( 2 ), "ms, using ", memUsed, "Kb"
|
||||||
discard l.fh.getFileHandle.lockf( F_ULOCK, 0 )
|
discard l.fh.getFileHandle.lockf( F_ULOCK, 0 )
|
||||||
l.fh.close()
|
l.fh.close()
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -58,8 +58,9 @@ type Message* = ref object
|
||||||
stream: FileStream
|
stream: FileStream
|
||||||
|
|
||||||
|
|
||||||
# Count messages generated during a run.
|
var
|
||||||
var msgcount = 0
|
msgcount = 0 # Count messages generated during a run.
|
||||||
|
msgId = "" # The parsed Message-ID
|
||||||
|
|
||||||
|
|
||||||
#############################################################
|
#############################################################
|
||||||
|
|
@ -212,7 +213,6 @@ proc filter*( orig_msg: Message, cmd: seq[string] ): Message =
|
||||||
proc parseHeaders*( msg: Message ) =
|
proc parseHeaders*( msg: Message ) =
|
||||||
## Walk the RFC2822 headers, placing them into memory.
|
## Walk the RFC2822 headers, placing them into memory.
|
||||||
## This 'unwraps' multiline headers, and allows for duplicate headers.
|
## This 'unwraps' multiline headers, and allows for duplicate headers.
|
||||||
let preparsed = not msg.headers.isNil
|
|
||||||
msg.headers = initTable[ string, seq[string] ]()
|
msg.headers = initTable[ string, seq[string] ]()
|
||||||
msg.open
|
msg.open
|
||||||
|
|
||||||
|
|
@ -232,8 +232,9 @@ proc parseHeaders*( msg: Message ) =
|
||||||
|
|
||||||
# Fold continuation line
|
# Fold continuation line
|
||||||
#
|
#
|
||||||
if line.startsWith( ' ' ) or line.startsWith( '\t' ):
|
let wsp = re"^\s+"
|
||||||
line = line.replace( re"^\s+" )
|
if line.match( wsp ):
|
||||||
|
line = line.replace( wsp )
|
||||||
value = value & ' ' & line
|
value = value & ' ' & line
|
||||||
|
|
||||||
# Header start
|
# Header start
|
||||||
|
|
@ -248,8 +249,10 @@ proc parseHeaders*( msg: Message ) =
|
||||||
msg.headers[ header ] = @[ value ]
|
msg.headers[ header ] = @[ value ]
|
||||||
( header, value ) = ( matches[0].toLower, matches[1] )
|
( header, value ) = ( matches[0].toLower, matches[1] )
|
||||||
|
|
||||||
|
if msgId == "":
|
||||||
"Parsed message headers.".debug
|
"Parsed message headers.".debug
|
||||||
if msg.headers.hasKey( "message-id" ):
|
if msg.headers.hasKey( "message-id" ):
|
||||||
|
msgId = msg.headers[ "message-id" ][0]
|
||||||
"Message-ID is \"$#\"".debug( msg.headers[ "message-id" ] )
|
"Message-ID is \"$#\"".debug( msg.headers[ "message-id" ] )
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -262,9 +265,9 @@ proc evalRules*( msg: var Message, rules: seq[Rule], default: Maildir ): bool =
|
||||||
for rule in rules:
|
for rule in rules:
|
||||||
var match = false
|
var match = false
|
||||||
|
|
||||||
if rule.headers.len > 0: "Evaluating rule...".debug
|
if rule.match.len > 0: "Evaluating rule...".debug
|
||||||
block thisRule:
|
block thisRule:
|
||||||
for header, regexp in rule.headers:
|
for header, regexp in rule.match:
|
||||||
let header_chk = header.toLower
|
let header_chk = header.toLower
|
||||||
var
|
var
|
||||||
hmatch = false
|
hmatch = false
|
||||||
|
|
@ -320,4 +323,5 @@ proc evalRules*( msg: var Message, rules: seq[Rule], default: Maildir ): bool =
|
||||||
deliver = default
|
deliver = default
|
||||||
|
|
||||||
msg.save( deliver )
|
msg.save( deliver )
|
||||||
|
return # stop processing additional rules
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -35,12 +35,21 @@ const
|
||||||
-d --debug:
|
-d --debug:
|
||||||
Debug: Be verbose while parsing.
|
Debug: Be verbose while parsing.
|
||||||
|
|
||||||
|
-g --generate:
|
||||||
|
Emit an example configuration file to stdout.
|
||||||
|
|
||||||
-h --help:
|
-h --help:
|
||||||
Help. You're lookin' at it.
|
Help. You're lookin' at it.
|
||||||
|
|
||||||
|
-l --log:
|
||||||
|
A file to record actions to, relative to $HOME/Maildir.
|
||||||
|
|
||||||
-v --version:
|
-v --version:
|
||||||
Display version number.
|
Display version number.
|
||||||
"""
|
"""
|
||||||
|
EXAMPLECONFIG = """
|
||||||
|
sdfdsfsdfsdfsdfdsfdf FIXME: FIXME WJSDFJKSDFKSDF
|
||||||
|
"""
|
||||||
|
|
||||||
#############################################################
|
#############################################################
|
||||||
# T Y P E S
|
# T Y P E S
|
||||||
|
|
@ -49,6 +58,14 @@ const
|
||||||
type Opts = object
|
type Opts = object
|
||||||
config*: string # The path to an explicit configuration file.
|
config*: string # The path to an explicit configuration file.
|
||||||
debug*: bool # Explain what's being done.
|
debug*: bool # Explain what's being done.
|
||||||
|
logfile*: string # Log actions to disk.
|
||||||
|
|
||||||
|
|
||||||
|
#############################################################
|
||||||
|
# G L O B A L E X P O R T S
|
||||||
|
#############################################################
|
||||||
|
|
||||||
|
var opts*: Opts
|
||||||
|
|
||||||
|
|
||||||
#############################################################
|
#############################################################
|
||||||
|
|
@ -74,24 +91,25 @@ proc deferral*( msg: string ) =
|
||||||
proc debug*( msg: string, args: varargs[string, `$`] ) =
|
proc debug*( msg: string, args: varargs[string, `$`] ) =
|
||||||
## Emit +msg+ if debug mode is enabled, coercing arguments into a string for
|
## Emit +msg+ if debug mode is enabled, coercing arguments into a string for
|
||||||
## formatting.
|
## formatting.
|
||||||
if defined( debug ) or not logger.closed:
|
if opts.debug or not logger.closed:
|
||||||
var str = msg % args
|
var str = msg % args
|
||||||
if defined( debug ): echo str
|
if opts.debug: echo str
|
||||||
if not logger.closed: str.log
|
if not logger.closed: str.log
|
||||||
|
|
||||||
|
|
||||||
proc parse_cmdline*: Opts =
|
proc parseCmdline*() =
|
||||||
## Populate the opts object with the user's preferences.
|
## Populate the opts object with the user's preferences.
|
||||||
|
|
||||||
# Config object defaults.
|
# Config object defaults.
|
||||||
#
|
#
|
||||||
result = Opts(
|
opts = Opts(
|
||||||
config: "",
|
config: "",
|
||||||
debug: false
|
debug: false,
|
||||||
|
logfile: ""
|
||||||
)
|
)
|
||||||
|
|
||||||
# always set debug mode if development build.
|
# always set debug mode if development build.
|
||||||
result.debug = defined( debug )
|
opts.debug = defined( debug )
|
||||||
|
|
||||||
for kind, key, val in getopt():
|
for kind, key, val in getopt():
|
||||||
case kind
|
case kind
|
||||||
|
|
@ -102,15 +120,22 @@ proc parse_cmdline*: Opts =
|
||||||
of cmdLongOption, cmdShortOption:
|
of cmdLongOption, cmdShortOption:
|
||||||
case key
|
case key
|
||||||
of "conf", "c":
|
of "conf", "c":
|
||||||
result.config = val
|
opts.config = val
|
||||||
|
|
||||||
of "debug", "d":
|
of "debug", "d":
|
||||||
result.debug = true
|
opts.debug = true
|
||||||
|
|
||||||
|
of "generate", "g":
|
||||||
|
echo EXAMPLECONFIG
|
||||||
|
quit( 0 )
|
||||||
|
|
||||||
of "help", "h":
|
of "help", "h":
|
||||||
echo USAGE
|
echo USAGE
|
||||||
quit( 0 )
|
quit( 0 )
|
||||||
|
|
||||||
|
of "log", "l":
|
||||||
|
opts.logfile = val
|
||||||
|
|
||||||
of "version", "v":
|
of "version", "v":
|
||||||
echo "Sieb " & VERSION
|
echo "Sieb " & VERSION
|
||||||
quit( 0 )
|
quit( 0 )
|
||||||
|
|
@ -119,4 +144,3 @@ proc parse_cmdline*: Opts =
|
||||||
|
|
||||||
of cmdEnd: assert( false ) # shouldn't reach here
|
of cmdEnd: assert( false ) # shouldn't reach here
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
40
src/sieb.nim
40
src/sieb.nim
|
|
@ -1,6 +1,11 @@
|
||||||
# vim: set et nosta sw=4 ts=4 :
|
# vim: set et nosta sw=4 ts=4 :
|
||||||
|
|
||||||
|
#############################################################
|
||||||
|
# I M P O R T S
|
||||||
|
#############################################################
|
||||||
|
|
||||||
import
|
import
|
||||||
|
std/exitprocs,
|
||||||
std/os
|
std/os
|
||||||
|
|
||||||
import
|
import
|
||||||
|
|
@ -9,37 +14,43 @@ import
|
||||||
lib/message,
|
lib/message,
|
||||||
lib/util
|
lib/util
|
||||||
|
|
||||||
# /home/mahlon/repo/sieb/src/sieb.nim(30) sieb
|
|
||||||
# /home/mahlon/.choosenim/toolchains/nim-1.6.10/lib/system/io.nim(759) open
|
|
||||||
# Error: unhandled exception: cannot open: /home/mahlon/ [IOError]
|
|
||||||
|
|
||||||
# TODO: timer/performance
|
|
||||||
# TODO: more performant debug
|
|
||||||
# TODO: generate default config?
|
|
||||||
|
|
||||||
|
#############################################################
|
||||||
|
# S E T U P
|
||||||
|
#############################################################
|
||||||
|
|
||||||
# Without this, we got nuthin'!
|
# Without this, we got nuthin'!
|
||||||
if not existsEnv( "HOME" ):
|
if not existsEnv( "HOME" ):
|
||||||
deferral "Unable to determine HOME from environment."
|
deferral "Fatal: Unable to determine HOME from environment."
|
||||||
|
|
||||||
|
# Populate $opts
|
||||||
|
parseCmdline()
|
||||||
|
|
||||||
let
|
let
|
||||||
home = getHomeDir()
|
home = getHomeDir()
|
||||||
opts = parse_cmdline()
|
|
||||||
conf = get_config( opts.config )
|
|
||||||
default = newMaildir( joinPath( home, "Maildir" ) )
|
default = newMaildir( joinPath( home, "Maildir" ) )
|
||||||
|
|
||||||
if conf.logfile != "":
|
# Open the optional log file.
|
||||||
createLogger( conf.logfile )
|
if opts.logfile != "": createLogger( default.path, opts.logfile )
|
||||||
|
|
||||||
|
# Exit hook - clean up any open logger filehandle.
|
||||||
|
var finalTasks = proc: void =
|
||||||
|
if not logger.closed: logger.close
|
||||||
|
finalTasks.addExitProc
|
||||||
|
|
||||||
|
|
||||||
# FIXME: at exit?
|
#############################################################
|
||||||
# ... if logger not nil logger close
|
# M A I N
|
||||||
|
#############################################################
|
||||||
|
|
||||||
|
# Parse the YAML ruleset.
|
||||||
|
let conf = getConfig( opts.config )
|
||||||
|
|
||||||
# Create a new message under Maildir/tmp, and stream stdin to it.
|
# Create a new message under Maildir/tmp, and stream stdin to it.
|
||||||
var msg = default.newMessage.writeStdin
|
var msg = default.newMessage.writeStdin
|
||||||
|
|
||||||
# If there are "early rules", parse the message now and walk those.
|
# If there are "early rules", parse the message now and walk those.
|
||||||
|
#
|
||||||
if conf.early_rules.len > 0:
|
if conf.early_rules.len > 0:
|
||||||
if msg.evalRules( conf.early_rules, default ): quit( 0 )
|
if msg.evalRules( conf.early_rules, default ): quit( 0 )
|
||||||
|
|
||||||
|
|
@ -47,6 +58,7 @@ if conf.early_rules.len > 0:
|
||||||
for filter in conf.filter: msg = msg.filter( filter )
|
for filter in conf.filter: msg = msg.filter( filter )
|
||||||
|
|
||||||
# Walk the rules, and if nothing hits, deliver to fallthrough.
|
# Walk the rules, and if nothing hits, deliver to fallthrough.
|
||||||
|
#
|
||||||
if conf.rules.len > 0:
|
if conf.rules.len > 0:
|
||||||
if not msg.evalRules( conf.rules, default ): msg.save
|
if not msg.evalRules( conf.rules, default ): msg.save
|
||||||
else:
|
else:
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue