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
|
|
@ -11,7 +11,6 @@
|
|||
import
|
||||
std/os,
|
||||
std/streams,
|
||||
std/strformat,
|
||||
std/tables,
|
||||
yaml/parser,
|
||||
yaml/serialization
|
||||
|
|
@ -35,13 +34,12 @@ const CONFFILES = @[
|
|||
|
||||
type
|
||||
Rule* = object
|
||||
headers* {.defaultVal: initTable[string, string]()}: Table[ string, string ]
|
||||
match* {.defaultVal: initTable[string, string]()}: Table[ string, string ]
|
||||
deliver* {.defaultVal: ""}: string
|
||||
filter* {.defaultVal: @[]}: seq[ seq[string] ]
|
||||
|
||||
# Typed configuration file layout for YAML loading.
|
||||
Config* = object
|
||||
logfile* {.defaultVal: "".}: string
|
||||
filter* {.defaultVal: @[]}: seq[ seq[string] ]
|
||||
early_rules* {.defaultVal: @[]}: seq[Rule]
|
||||
rules* {.defaultVal: @[]}: seq[Rule]
|
||||
|
|
@ -53,7 +51,7 @@ type
|
|||
|
||||
proc parse( path: string ): Config =
|
||||
## Return a parsed configuration from yaml.
|
||||
debug "Using configuration at: {path}".fmt
|
||||
"Using configuration at: $#".debug( path )
|
||||
let stream = newFileStream( path )
|
||||
try:
|
||||
stream.load( result )
|
||||
|
|
@ -61,18 +59,18 @@ proc parse( path: string ): Config =
|
|||
debug err.msg
|
||||
return Config() # return empty default, it could be "half parsed"
|
||||
except YamlConstructionError as err:
|
||||
debug err.msg
|
||||
err.msg.debug
|
||||
return Config()
|
||||
finally:
|
||||
stream.close
|
||||
|
||||
|
||||
proc get_config*( path: string ): Config =
|
||||
proc getConfig*( path: string ): Config =
|
||||
## Choose a configuration file for parsing, or if there are
|
||||
## none available, return an empty config.
|
||||
if path != "":
|
||||
if not path.fileExists:
|
||||
debug "Configfile \"{path}\" unreadable, ignoring.".fmt
|
||||
"Configfile \"$#\" unreadable, ignoring.".debug( path )
|
||||
return
|
||||
return parse( path )
|
||||
|
||||
|
|
|
|||
|
|
@ -8,6 +8,8 @@
|
|||
#############################################################
|
||||
|
||||
import
|
||||
std/math,
|
||||
std/monotimes,
|
||||
std/os,
|
||||
std/posix,
|
||||
std/times
|
||||
|
|
@ -19,6 +21,7 @@ import
|
|||
|
||||
type Logger = object
|
||||
fh: File
|
||||
start: MonoTime
|
||||
|
||||
|
||||
#############################################################
|
||||
|
|
@ -32,21 +35,24 @@ var logger*: Logger
|
|||
# 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+.
|
||||
## This will block until it can get an exclusive lock.
|
||||
let path = joinPath( getHomeDir(), path )
|
||||
logger = Logger()
|
||||
logger.fh = path.open( fmAppend )
|
||||
let path = joinPath( parentdir, filename )
|
||||
logger = Logger()
|
||||
logger.fh = path.open( fmAppend )
|
||||
logger.start = getMonoTime()
|
||||
|
||||
# Wait for exclusive lock.
|
||||
discard logger.fh.getFileHandle.lockf( F_LOCK, 0 )
|
||||
logger.fh.writeLine "\n-------------------------------------------------------------------"
|
||||
logger.fh.writeLine now().utc
|
||||
logger.fh.writeLine "\n", now().utc, " ------------------------------------------------------------------"
|
||||
|
||||
|
||||
proc close*( l: Logger ): void =
|
||||
## 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 )
|
||||
l.fh.close()
|
||||
|
||||
|
|
|
|||
|
|
@ -58,8 +58,9 @@ type Message* = ref object
|
|||
stream: FileStream
|
||||
|
||||
|
||||
# Count messages generated during a run.
|
||||
var msgcount = 0
|
||||
var
|
||||
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 ) =
|
||||
## Walk the RFC2822 headers, placing them into memory.
|
||||
## This 'unwraps' multiline headers, and allows for duplicate headers.
|
||||
let preparsed = not msg.headers.isNil
|
||||
msg.headers = initTable[ string, seq[string] ]()
|
||||
msg.open
|
||||
|
||||
|
|
@ -232,8 +232,9 @@ proc parseHeaders*( msg: Message ) =
|
|||
|
||||
# Fold continuation line
|
||||
#
|
||||
if line.startsWith( ' ' ) or line.startsWith( '\t' ):
|
||||
line = line.replace( re"^\s+" )
|
||||
let wsp = re"^\s+"
|
||||
if line.match( wsp ):
|
||||
line = line.replace( wsp )
|
||||
value = value & ' ' & line
|
||||
|
||||
# Header start
|
||||
|
|
@ -248,9 +249,11 @@ proc parseHeaders*( msg: Message ) =
|
|||
msg.headers[ header ] = @[ value ]
|
||||
( header, value ) = ( matches[0].toLower, matches[1] )
|
||||
|
||||
"Parsed message headers.".debug
|
||||
if msg.headers.hasKey( "message-id" ):
|
||||
"Message-ID is \"$#\"".debug( msg.headers[ "message-id" ] )
|
||||
if msgId == "":
|
||||
"Parsed message headers.".debug
|
||||
if msg.headers.hasKey( "message-id" ):
|
||||
msgId = msg.headers[ "message-id" ][0]
|
||||
"Message-ID is \"$#\"".debug( msg.headers[ "message-id" ] )
|
||||
|
||||
|
||||
proc evalRules*( msg: var Message, rules: seq[Rule], default: Maildir ): bool =
|
||||
|
|
@ -262,9 +265,9 @@ proc evalRules*( msg: var Message, rules: seq[Rule], default: Maildir ): bool =
|
|||
for rule in rules:
|
||||
var match = false
|
||||
|
||||
if rule.headers.len > 0: "Evaluating rule...".debug
|
||||
if rule.match.len > 0: "Evaluating rule...".debug
|
||||
block thisRule:
|
||||
for header, regexp in rule.headers:
|
||||
for header, regexp in rule.match:
|
||||
let header_chk = header.toLower
|
||||
var
|
||||
hmatch = false
|
||||
|
|
@ -320,4 +323,5 @@ proc evalRules*( msg: var Message, rules: seq[Rule], default: Maildir ): bool =
|
|||
deliver = default
|
||||
|
||||
msg.save( deliver )
|
||||
return # stop processing additional rules
|
||||
|
||||
|
|
|
|||
|
|
@ -35,20 +35,37 @@ const
|
|||
-d --debug:
|
||||
Debug: Be verbose while parsing.
|
||||
|
||||
-g --generate:
|
||||
Emit an example configuration file to stdout.
|
||||
|
||||
-h --help:
|
||||
Help. You're lookin' at it.
|
||||
|
||||
-l --log:
|
||||
A file to record actions to, relative to $HOME/Maildir.
|
||||
|
||||
-v --version:
|
||||
Display version number.
|
||||
"""
|
||||
EXAMPLECONFIG = """
|
||||
sdfdsfsdfsdfsdfdsfdf FIXME: FIXME WJSDFJKSDFKSDF
|
||||
"""
|
||||
|
||||
#############################################################
|
||||
# T Y P E S
|
||||
#############################################################
|
||||
|
||||
type Opts = object
|
||||
config*: string # The path to an explicit configuration file.
|
||||
debug*: bool # Explain what's being done.
|
||||
config*: string # The path to an explicit configuration file.
|
||||
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, `$`] ) =
|
||||
## Emit +msg+ if debug mode is enabled, coercing arguments into a string for
|
||||
## formatting.
|
||||
if defined( debug ) or not logger.closed:
|
||||
if opts.debug or not logger.closed:
|
||||
var str = msg % args
|
||||
if defined( debug ): echo str
|
||||
if opts.debug: echo str
|
||||
if not logger.closed: str.log
|
||||
|
||||
|
||||
proc parse_cmdline*: Opts =
|
||||
proc parseCmdline*() =
|
||||
## Populate the opts object with the user's preferences.
|
||||
|
||||
# Config object defaults.
|
||||
#
|
||||
result = Opts(
|
||||
opts = Opts(
|
||||
config: "",
|
||||
debug: false
|
||||
debug: false,
|
||||
logfile: ""
|
||||
)
|
||||
|
||||
# always set debug mode if development build.
|
||||
result.debug = defined( debug )
|
||||
opts.debug = defined( debug )
|
||||
|
||||
for kind, key, val in getopt():
|
||||
case kind
|
||||
|
|
@ -102,15 +120,22 @@ proc parse_cmdline*: Opts =
|
|||
of cmdLongOption, cmdShortOption:
|
||||
case key
|
||||
of "conf", "c":
|
||||
result.config = val
|
||||
opts.config = val
|
||||
|
||||
of "debug", "d":
|
||||
result.debug = true
|
||||
opts.debug = true
|
||||
|
||||
of "generate", "g":
|
||||
echo EXAMPLECONFIG
|
||||
quit( 0 )
|
||||
|
||||
of "help", "h":
|
||||
echo USAGE
|
||||
quit( 0 )
|
||||
|
||||
of "log", "l":
|
||||
opts.logfile = val
|
||||
|
||||
of "version", "v":
|
||||
echo "Sieb " & VERSION
|
||||
quit( 0 )
|
||||
|
|
@ -119,4 +144,3 @@ proc parse_cmdline*: Opts =
|
|||
|
||||
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 :
|
||||
|
||||
#############################################################
|
||||
# I M P O R T S
|
||||
#############################################################
|
||||
|
||||
import
|
||||
std/exitprocs,
|
||||
std/os
|
||||
|
||||
import
|
||||
|
|
@ -9,37 +14,43 @@ import
|
|||
lib/message,
|
||||
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'!
|
||||
if not existsEnv( "HOME" ):
|
||||
deferral "Unable to determine HOME from environment."
|
||||
deferral "Fatal: Unable to determine HOME from environment."
|
||||
|
||||
# Populate $opts
|
||||
parseCmdline()
|
||||
|
||||
let
|
||||
home = getHomeDir()
|
||||
opts = parse_cmdline()
|
||||
conf = get_config( opts.config )
|
||||
default = newMaildir( joinPath( home, "Maildir" ) )
|
||||
|
||||
if conf.logfile != "":
|
||||
createLogger( conf.logfile )
|
||||
# Open the optional log file.
|
||||
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.
|
||||
var msg = default.newMessage.writeStdin
|
||||
|
||||
# If there are "early rules", parse the message now and walk those.
|
||||
#
|
||||
if conf.early_rules.len > 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 )
|
||||
|
||||
# Walk the rules, and if nothing hits, deliver to fallthrough.
|
||||
#
|
||||
if conf.rules.len > 0:
|
||||
if not msg.evalRules( conf.rules, default ): msg.save
|
||||
else:
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue