Overview | Index by: file name | procedure name | procedure call | annotation
helper.tcl (annotations | original source)

#
#    Copyright (C) 2010 Alexandros Stergiakis <alsterg@gmail.com>
#
#    This program is free software: you can redistribute it and/or modify
#    it under the terms of the GNU Affero General Public License as
#    published by the Free Software Foundation, either version 3 of the
#    License, or (at your option) any later version.
#
#    This program is distributed in the hope that it will be useful,
#    but WITHOUT ANY WARRANTY; without even the implied warranty of
#    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
#    GNU Affero General Public License for more details.
#
#    You should have received a copy of the GNU Affero General Public License
#    along with this program.  If not, see <http://www.gnu.org/licenses/>.
#

#//#
# Procedures that facilitate module writting. It should be loaded from the Master only,
# not Sessions.
#//#

namespace eval helper {
    # Misc
    namespace export parseconf secs2age
    
    # File Operations
    namespace export checkfs removeline
    
    # Handle blocking
    namespace export timeout blocks break_event
    
    # Process Control
    namespace export kill killall isrunning ptyexec pidof
    
    # Dependency checking
    namespace export kernel_has busybox_has
    
    # Argument type checking
    namespace export ishostname isdomainname isip isip4 isip6 isipport isemail
    
    # Access of Session I/O from Master
    namespace export sget sputs sflush sread ask

interp create -safe -- ::helper::parseconf_interp

# Parse a configuration file consisting of instructions of the form var = val.
# Load these definitions in the $gvar shared memory array, that belongs to
# corresponding MikroConf module.
#
# The "val" part of each assignment can refer to other variables that belong to
# different MikroConf modules. But in order for these other variables to be
# in-scope, the corresponding module name (or modules as a list) must be provided 
# as the last argument.
#
# Note: Once the variables of module A become accessible in module B, then
# a third module C that loads B's variables, will load A's as well, automatically.
#
# Note: The "conf" shared memory array is accessible to all modules by default.
#
# @param gvar The name of the shared memory array.
# @param filepath The name/path of the configuration file to parse.
# @param args List of MikroConf module names whose variables should be in scope of \
  the parsed configuration instructions. The variable assignments in MikroConf \
  initialization file is by default within scope.
proc parseconf {gvar filepath args} {
    if {$gvar ne "conf"} {
        lappend args conf ;# conf is within scope by default.
    }
    parseconf_interp eval [list namespace eval $gvar {}]
    foreach ns $args {
        foreach var [parseconf_interp eval [list info vars ${ns}::*]] {
            parseconf_interp eval [list namespace inscope $gvar [list upvar $var [namespace tail $var]]]
        }
    }
    foreach line [split [::fileutil::cat $filepath] "\n"] {
        if {[lempty $line] || [string match {#*} $line]} {
            continue
        }
        lassign [split $line =] var val
        set var [string trim $var]
        set val [string trim $val]
        set val [parseconf_interp eval [list namespace inscope $gvar [list subst -nocommands $val]]]
        parseconf_interp eval [list namespace inscope $gvar [list variable $var $val]]
        tsv::set $gvar $var $val
    }
}

# Check that a filesystem entity exists and it has specific properties.
# The following flags are supported:
#   r : Entity is readable
#   w : Entity is writable
#   x : Entity is executable
#   f : Entity is a file
#   d : Entity is a directory
#   c : Create entity if it does not exist
# Any combinatio of these flags can be used.
#
# @param type "-dir" or "-file"
# @param path The path of the entity.
# @param flags A list of flags to check.
# @param text The text to write to the file, if it is being created (flag 'c').
# @error
proc checkfs {type path {flags {}} {text {}}} {
    if {! [file exists $path]} {
        if {"c" in $flags} {
            log::Info "Warning: $path does not exist; creating now.."
            switch -exact -- $type {
                "-dir" {file mkdir $path}
                "-file" {write_file "$path" "$text"}
                default {error "unrecognized type: $type"}
            }
        } else {
            error "$path does not exist"
        }
    }

    switch -exact -- $type {
        "-dir" {
            if {! [file isdirectory $path]} {
                error "$path is not a directory"
            }
        }
        "-file" {
            if {! [file isfile $path]} {
                error "$path is not a file"
            }
        }
        default {error "unrecognized type: $type"}
    }
        
    foreach f $flags {
        switch -- $f {
            "r" {
                if {! [file readable $path]} {
                    error "$path file is not readable"
                }
            }
            "w" {
                if {! [file writable $path]} {
                    error "$path file is not writable"
                }
            }
            "x" {
                if {! [file executable $path]} {
                    error "$path file is not executable"
                }
            }
            "c" {}
            default {
                error "unrecognized option $f"
            }
        }
    }
}

# Checks that a string is a valid IP hostname.
#
# Validity:
# Hostnames, like all domain names[1], are made up of a series of "labels",
# with each label being separated by a dot. Each label must be between 1 and 63
# characters long, and there is a maximum of 255 characters when all labels are combined.
# Unlike domain names, hostname labels can only be made up of the ASCII letters 'a' through 'z'
# (case-insensitive), the digits '0' through '9', and the hyphen. Labels cannot start nor end
# with a hyphen. Special characters other than the hyphen (and the dot between labels) are
# not allowed, although they are sometimes used anyway. 
#
# @todo How many labels?
# @todo Can a label start with [0-9]? (currently we don't allow)?
#
# @link http://tools.ietf.org/html/rfc952
#
# @param str The input string to check.
# @return '1' is it is hostname is legal, otherwise '0'.
proc ishostname {str} {
    set RE {^[a-zA-Z]([a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(\.[a-zA-Z]([a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*$}
    return [expr {0 < [string length $str] <= 255 && [regexp $RE $str]}]
}

# Checks that a string is a valid IP domain name.
#
# Validity:
# Like hostnames (see above) but also the underscore character can be used.
#
# @todo Verify validity.
# @todo Support for international domain names (e.g. with greek characters in them)
#
# @link http://tools.ietf.org/html/rfc1034
#
# @param str The input string to check.
# @return '1' is it is hostname is legal, otherwise '0'.
proc isdomainname {str} {
    set RE {^[a-zA-Z]([a-zA-Z0-9_-]{0,61}[a-zA-Z0-9])?(\.[a-zA-Z]([a-zA-Z0-9_-]{0,61}[a-zA-Z0-9])?)*$}
    return [expr {0 < [string length $str] <= 255 && [regexp $RE $str]}]
}

# Checks if an string represents a legal IPv4 address.
#
# @param str The input string to check.
# @return '1' is it is a legal IPv4, otherwise '0'.
proc isip4 {str} {
    set octet {(\d|[1-9]\d|1\d\d|2[0-4]\d|25[0-5])}
    set RE "^[join [list $octet $octet $octet $octet] {\.}]\$"
    return [regexp $RE $str]
}

# Checks if an string represents a legal IPv6 address.
#
# @param str The input string to check.
# @return '1' is it is a legal IPv6, otherwise '0'.
proc isip6 { str } {
    expr {[ip::version $str] == 6}
}

# Checks if an string represents a legal IP address (v4 or v6).
#
# @param str The input string to check.
# @return '1' is it is a legal IP, otherwise '0'.
proc isip {str} {
    return [expr {[isip4 $str] || [isip6 $str]}]
}

# Checks if a string has a format that is compatible with normal RFC SMTP email address formats.
#
# @limit It does not account for comments embedded within email \
addresses, which are defined even though seldom used.
#
# @param str The input string to check.
# @return '1' is it is valid email address, otherwise '0'.
proc isemail {str} {
    return [regexp {^[A-Za-z0-9._-]+@[A-Za-z0-9.-]+$} $str]
}

# Check if a string is a valid TCP/UDP port number.
#
proc isipport {port} {
    return [expr {[string is integer $port] && (0 <= $port <= 65535)}]
}

# Remove lines from a file that match a given pattern. Update file in place.
# A temporary file $filepath.new is used internally.
#
# @param filepath The filepath of the file. The file must pre-exist.
# @param pattern A "string match" pattern.
# @error
proc removeline {filepath pattern} {
    set fd [open "${filepath}.new" w]
    if {[catch {
        foreach line [split [::fileutil::cat $filepath] "\n"] {
            if {! [string match $pattern $line]} {
                puts $fd $line
            }
        }
    } errMsg errStack]} {
        close $fd
        return -options $errStack $errMsg
    }
    close $fd
    
    if {[catch {
        file rename -force ${filepath}.new $filepath
    } errMsg errStack]} {
        file delete -force ${filepath}.new
        return -options $errStack $errMsg
    }
}

# Convert a duration in secs to a string describing the duration, involving days, hours, mins & secs.
#
# @assume Input argument is sane.
# @param secs Absolute number of seconds.
# @param format Requests a specific format for the returned string. Supported: -full -simple
# @return A string.
proc secs2age {secs {format -full}} {
    set days [expr {$secs / 86400}]
    set secs [expr {$secs % 86400}]
    set hours [expr {$secs / 3600}]
    set secs [expr {$secs % 3600}]
    set mins [expr {$secs / 60}]
    set secs [expr {$secs % 60}]
    
    set result {}
    switch -exact -- $format {
        "-full" {
            if {$days != 0} { append result "$days Days " }
            if {$hours != 0} { append result "$hours Hours " }
            if {$mins != 0} { append result "$mins Mins " }
            append result "$secs Secs "
        }
        "-simple" {
            if {$days != 0} { append result "${days}D" }
            if {$hours != 0} { append result "${hours}H" }
            if {$mins != 0} { append result "${mins}M" }
            append result "${secs}S"
        }
    }
    
    return $result
}

# Execute a script in the context of the caller, but wait up to $secs number of secs 
# for the script to finish evaluation. If after $secs seconds the script is still executing
# then it will be forcefully interrupted. The interruption will seem to the caller like an
# error that the script generated.
#
# This procedure should be used always to avoid blocking the Master for an unknown
# period of time. However notice that this procedure alone doesn't safeguards the Master
# from blocking. See ::helper::blocks for mitigating this.
#
# @todo Make to interrupt script evaluation when Ctrl ^ + x is presssed.
# @param secs Number of seconds to wait.
# @param script Script to execute.
# @error
proc timeout {secs script} {    
    if {[catch {
        signal trap SIGALRM {error "Timeout waiting"}
        alarm $secs
        uplevel $script
    } result options]} {
        # Either timeout or an error while evaluating the script.
        signal ignore SIGALRM ;# For the case of error
        return -options $options $result
    }
    signal ignore SIGALRM
}

proc blocks {script} {
# XXX
    uplevel $script
}

proc break_event {sid} {
# XXX
}

# Check that the running kernel supports a specific feature.
#
# @param feature The name of the feature to be checked, e.g. CONFIG_INOTIFY.
# @return '1' if supported, '0' otherwise.
# @error
proc kernel_has {feature} {
    Global KERNEL_CHKS
    if {! $KERNEL_CHKS} { return 1 }
    global KCONFIG
    return [expr {("${feature}=y" in $KCONFIG) || ("${feature}=m" in $KCONFIG)}]
}

# Check that the installed busybox supports a specific feature.
#
# @param feature The name of the feature to be checked, e.g. CONFIG_ASH
# @return '1' if supported, '0' otherwise.
# @error
proc busybox_has {feature} {
    Global BUSYBOX_CHKS
    if {! $BUSYBOX_CHKS} { return 1 }
    global BCONFIG
    return [expr {"${feature}=y" in $BCONFIG}]
}

# Find if a certain executable (usually a daemon) is running.
#
# @param name Name of executable e.g. syslogd
proc isrunning {name} {
    if {[catch {exec pidof $name}]} { ;# 'pidof' exits with error if no process found.
        return 0
    } else {
        return 1
    }
}

# Return a list of process ids for all the processes with executable $name.
#
# @param name Name of executable e.g. syslogd
# @return A list of PIDs, or the empty list.
proc pidof {name} {
    if {[catch {
        exec pidof $name
    } result]} {
        return
    }
    return $result
}

# Send a signal to a process.
# @todo Check to see if it was actually killed, and log error if not.
proc kill {sig ps} {
    log::Debug "Killing process $ps ($sig)"
    exec kill -${sig} $ps
}

# Kill all processses with the specified name.
# First try -TERM, then try -KILL. Log errors.
#
# @param name Name of executable e.g. syslogd
# @todo Check to see if it was actually killed, and log error if not.
proc killall {name} {
    log::Debug "Killing process $name"
    catch { exec killall -q -TERM $name }
    catch { exec killall -q -KILL $name }
}

# Access the version of "flush" of the Session.
#
# @param args The standard "flush" arguments.
proc sflush {args} {
    # Note: The operation here must be asynchronous, becase these wrappers
    # are used from log, emit .. which can be initiated from within Session/Slave.
    set sid [::session::get_sessionId]
    Thread::bgsend $sid [list flush {*}$args]
}

# Access the version of "gets" of the Session.
#
# @param args The standard "flush" arguments.
proc sgets {args} {
    # Note: The operation here must be asynchronous, becase these wrappers
    # are used from log, emit .. which can be initiated from within Session/Slave.
    set sid [::session::get_sessionId]
    Thread::bgsend $sid [list nbgets {*}$args]
}

# Access the version of "read" of the Session.
#
# @param args The standard "flush" arguments.
proc sread {args} {
    # Note: The operation here must be asynchronous, becase these wrappers
    # are used from log, emit .. which can be initiated from within Session/Slave.
    set sid [::session::get_sessionId]
    Thread::bgsend $sid [list read {*}$args]
}

# Access the version of "puts" of the Session.
#
# @param args The standard "flush" arguments.
proc sputs {args} {
    # Note: The operation here must be asynchronous, becase these wrappers
    # are used from log, emit .. which can be initiated from within Session/Slave.
    set sid [::session::get_sessionId]
    Thread::bgsend $sid [list more {*}$args]
}

# A convenience proc to prompt the user for some value.
# This is an interactive procedure that operates in a Session thread.
#
#   ask ?options? question
#
# The following options are supported:
#   -type <type> : The answer is accepted only if it is of type <type>, which can
#       be any of the types supported by "string is <type>". Default type: "ascii".
#   -default <value> : Specifies the default value which is used when the user
#       presses enter without providing a value.
#   -list <list> : The list of possible answers (can be used in conjuction with -type,
#       or on its own).
#   -min <min> : When the answer is a number, this is the minimum acceptable.
#   -max <max> : When the answer is a number, this is the maximum acceptable.
#   -check <proc> : Execute the specifies procedure with a single argument the
#       user's answer, and it should return a boolean. If true the answer is valid,
#       otherwise is not acceptable.
#   -eval <string> : Similar to -check , but this is a Tcl script that is evaluated on
#       global scope. Any occurence of %% inside <string> is substituted with the
#       provided answer by the user.
#   -switch <switch> : This switch simplifies execution of external programs that
#       accept command line switches, that depend on user's answers. For boolean
#       answers "ask" will return <switch> verbatim, whereas for any other type it will
#       return "<switch> <answer>".
#   -neg : When -type is boolean, this switch will reverse the normal return value.
#       Instead of returning <switch> when answer is true, it returns it when it evaluates
#       to false.
#   -noswitch : When used together with -default, this switch will cause "ask" to return
#       nothing (no switch), when the user provided the default value.
#   -hidedef : This will hide the the square brackets after the question with the default answer.
#   -nonewline : See the "nbgets" procedure in thread.tcl.
#   -noecho : See the "nbgets" procedure in thread.tcl.
#   -end : See the "nbgets" procedure in thread.tcl.
#   -length : See the "nbgets" procedure in thread.tcl.
#
# @param args See description above.
# @return Either the empty list or a string.
# @error
proc ask {args} {
    array set Param {
        "-type" ascii
        "-end" "\n"
    }
    
    set switches {}
    for {set i 0} {$i < [llength $args]} {incr i} {
        set arg [lindex $args $i]
        switch -exact -- $arg {
            "-type" -
            "-default" -
            "-list" -
            "-min" -
            "-max" -
            "-check" -
            "-eval" -
            "-switch" {
                set Param($arg) [lindex $args [incr i]]
            }
            "-neg" -
            "-hidedef" -
            "-noswitch" {
                set Param($arg) 1
            }
            "-nonewline" -
            "-noecho" {
                lappend switches $arg
            }
            "-end" -
            "-length" {
                lappend switches $arg [lindex $args [incr i]]
            }
            default {
                set question $arg
                break
            }
        }
    }
    
    for {} {1} {} {
        sputs -nonewline $question
        if {[info exists Param(-default)] && ! [info exists Param(-hidedef)]} {
            sputs -nonewline "\[$Param(-default)\] "
        }
        
        if {[catch {
            set answer [sgets {*}$switches]
        } errMsg errStack]} {
            sputs "" ;# newline
            return -options $errStack $errMsg
        }
        
        # Enforce default.
        if {[lempty $answer]} {
            if {[info exists Param(-default)]} {
                set answer $Param(-default)
            } else {
                continue
            }
            
        } else {
            # Check type.
            if {! [string is $Param(-type) $answer]} {
                sputs "illegal value; wrong type"
                continue
            }
            
            # Check min.
            if {[info exists Param(-min)]} {
                if {$answer < $Param(-min)} {
                    sputs "out of range; minimum allowed is: $Param(-min)"
                    continue
                }
            }
    
            # Check max.
            if {[info exists Param(-max)]} {
                if {$answer > $Param(-max)} {
                    sputs "out of range; maximum allowed is: $Param(-max)"
                    continue
                }
            }
            
            # Check "-list".
            if {[info exists Param(-list)]} {
                if {$answer ni $Param(-list)} {
                    sputs "illegal value; not one of possible options"
                    continue
                }
            }
            
            # Check "-check".
            if {[info exists Param(-check)]} {
                if {! [$Param(-check) $answer]} {
                    sputs "illegal value"
                    continue
                }
            }
            
            # Check "-eval".
            if {[info exists Param(-eval)]} {
                set txt [string map "%% {$answer}" $Param(-eval)]
                if {! [eval $txt]} {
                    sputs "illegal value"
                    continue
                }
            }
        }
        
        if {$Param(-type) eq "boolean"} {
            if {[info exists Param(-switch)]} {
                if {[info exists Param(-noswitch)] && [info exists Param(-default)] && $answer eq $Param(-default)} {
                    return
                }
                if {bool($answer)} {
                    if {[info exists Param(-neg)]} {
                        return
                    }
                    return $Param(-switch)
                } else {
                    if {[info exists Param(-neg)]} {
                        return $Param(-switch)
                    }
                    return
                }
            }
            return $answer
        } else {
            if {[info exists Param(-noswitch)] && [info exists Param(-default)] && $answer eq $Param(-default)} {
                return
            }
            if {[info exists Param(-switch)]} {
                return "$Param(-switch) $answer"
            }
            return $answer
        }
    }
}

} ;# End of Namespace

Overview | Index by: file name | procedure name | procedure call | annotation
File generated 2010-03-13 at 22:28.