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.