# copyright (C) 1997-2006 Jean-Luc Fontaine (mailto:jfontain@free.fr)
# this program is free software: please read the COPYRIGHT file enclosed in this package or use the Help Copyright menu

# $Id: snmp.tcl,v 2.56 2006/04/03 22:54:28 jfontain Exp $


package provide snmp [lindex {$Revision: 2.56 $} 1]
set version [package require Tnm]
if {[package vcompare $version 2.1.10] < 0} {
    error {Tnm version 2.1.10 or above is required}
}
set tnm3 [expr {[package vcompare $version 3] >= 0}]                                                            ;# tested on windows
if {$tnm3} {                                                                                  ;# commands are in their own namespace
    namespace import Tnm::*
    package require miscellaneous
}
package require hashes

proc listRemovedDuplicates {list} {
    set return {}
    foreach element $list {
        if {[lsearch -exact $return $element] < 0} {
            lappend return $element
        }
    }
    return $return
}

namespace eval snmp {

    array set data {
        updates 0
        switches {
            -a 1 --address 1 --community 1 --context 1 --delay 1 --directory 1 -i 1 --identifiers 1 --mibs 1 --port 1 --password 1
            --retries 1 -t 1 --table 1 --timeout 1 --title 1 --trace 0 --trim 1 --user 1 --version 1 --window 1
        }
        persistent 1 64Bits 1
        pollTimes {10 5 20 30 60 120 300}
    }
    set file [open snmp.htm]
    set data(helpText) [read $file]                                                           ;# initialize HTML help data from file
    close $file; unset file

if {$::tnm3} {

    proc mib {args} {                                      ;# override original command since -exact switch has disappeared in Tnm 3
        if {[lindex $args 0] eq "-exact"} {
            # emulate the -exact function for the following sub-command used in this module (add more if necessary):
            switch [lindex $args 1] {children - index - parent - syntax {
                if {[lindex [Tnm::mib split [lindex $args 2]] end] ne ""} {
                    error "instance identifier not allowed: $args"
                }
            }}
            return [eval Tnm::mib [lrange $args 1 end]]
        } else {
            return [eval Tnm::mib $args]
        }
    }

}

    proc reportError {identifier message} {
        if {$identifier ne ""} {
            set text "mib: [mib file $identifier]\n"
        }
        append text $message
        error $text
    }

    proc initialize {optionsName} {
        upvar 1 $optionsName options
        variable data
        variable session
        variable trace

        catch {set trace $options(--trace)}
        set directory {}; catch {set directory $options(--directory)}                                ;# optional MIB files directory
        set mibs {}; catch {set mibs [split $options(--mibs) ,]}                           ;# comma separated list of MIB file names
        foreach file [mibFiles $directory $mibs] {
            if {![file isfile $file]} {
                error "$file: not a regular file"
            }
            if {[catch {mib load $file} message]} {
                reportError {} $message
            }
        }
        catch {set table $options(-t)}
        catch {set table $options(--table)}                                                                     ;# favor long option
        set string {}
        catch {set string $options(-i)}
        catch {set string $options(--identifiers)}                                                              ;# favor long option
        set trim {}
        catch {set trim $options(--trim)}
        if {[info exists table]} {
            processTableIdentifiers $table $string $trim
        } else {
            if {$string eq ""} {
                reportError {} {table and/or list of identifiers must be specified}
            }
            processIdentifiers $string $trim
        }
        set arguments {}
        set address 127.0.0.1                                                                                          ;# by default
        catch {set address $options(-a)}
        catch {set address $options(--address)}                                                                 ;# favor long option
        set arguments [list -address $address]
        foreach switch {community context delay port password retries timeout window} {
            catch {lappend arguments -$switch $options(--$switch)}
        }
        if {[info exists options(--version)]} {                                                     ;# default is 1 (SNMP version 1)
            switch [string tolower $options(--version)] {
                1 {}
                2c {lappend arguments -version SNMPv2C}
                2u {lappend arguments -version SNMPv2U}
                default {
                    reportError {} {version must be 1, 2C or 2U}
                }
            }
        }
        if {$::tnm3} {
            set session [eval snmp generator $arguments]
        } else {
            set session [eval snmp session $arguments]
        }
        if {[info exists options(--title)]} {
            if {[string match *t* $options(--title)] && ![info exists table]} {
                reportError {} {--title option including table name but no table was specified (-t or --table option)}
            }
            switch $options(--title) {
                a {set data(identifier) snmp($address)}
                t {set data(identifier) snmp($table)}
                at {set data(identifier) snmp($address,$table)}
                ta {set data(identifier) snmp($table,$address)}
                default {
                    reportError {} {--title option must be a combination of the 'a' and 't' letters}
                }
            }
        }
    }

    proc validateIdentifier {name {exact 1}} {                          ;# report an error if identifier is not part of a loaded MIB
        if {$exact} {
            set invalid [catch {mib -exact syntax $name} message]
        } else {
            set invalid [catch {mib syntax $name} message]
        }
        if {$invalid} {
            reportError {} $message
        }
    }

    proc processIdentifiers {string trim} {
        # string format is identifier,identifier,...,identifier,,identifier,identifier,... with , to separate columns and ,, tables
        variable data
        variable requestIdentifiers
        variable indexColumns
        variable counter

        regsub -all ,, $string | string
        if {$::tnm3} {
            set command children
        } else {
            set command successor
        }
        foreach list [split $string |] {
            set identifiers {}
            foreach identifier [listRemovedDuplicates [split $list ,]] {
                validateIdentifier $identifier 0                            ;# no strict lookup since identifier can be instantiated
                if {[llength [mib index $identifier]] > 0} {
                    reportError $identifier "$identifier is a table: use the -t (--table) switch"
                }
                if {[mib access $identifier] eq "not-accessible"} {                                         ;# object may be a group
                    set parent $identifier
                    set invalid 1
                    foreach identifier [mib $command $parent] {                                  ;# lookup immediate successors only
                        if {[mib access $identifier] ne "not-accessible"} {                            ;# ignore tables, groups, ...
                            if {$::tnm3} {
                                lappend identifiers [mib label $identifier]                                     ;# remove MIB header
                            } else {
                                lappend identifiers $identifier
                            }
                            set invalid 0
                        }
                    }
                    if {$invalid} {
                        reportError $parent "$parent contains no accessible immediate successors"
                    }
                } else {
                    lappend identifiers $identifier
                }
            }
            lappend views $identifiers
        }
        set requestIdentifiers {}
        array set data {0,label {} 0,type ascii 0,message {}}   ;# use an empty hidden column as index as there is only a single row
        set nextColumn 1
        foreach list $views {
            set columns {}
            foreach identifier $list {
                set index [lsearch -exact $requestIdentifiers $identifier]                      ;# index in request identifiers list
                if {$index < 0} {                                                                                 ;# not yet in list
                    set index [llength $requestIdentifiers]
                    if {[regexp {\.\d+$} $identifier]} {                                                             ;# instanciated
                        lappend requestIdentifiers $identifier
                    } else {
                        lappend requestIdentifiers $identifier.0                                ;# complete with instance identifier
                    }
                }
                if {[catch {set column $identifierColumn($index)}]} {         ;# identifiers thus columns may be duplicated in views
                    set column $nextColumn
                    set identifierColumn($index) $column
                    incr nextColumn
                }
                lappend indexColumns($index) $column                                      ;# there can be identical columns in views
                regsub ^$trim $identifier {} data($column,label)                       ;# trim string from left side of column title
                # no strict lookup since identifier can be instantiated
                set data($column,type) [identifierType $identifier counter($index) 0]
                set data($column,message) [identifierMessage $identifier $counter($index)]
                switch $data($column,type) {
                    integer - real {
                        # display numeric values centered
                    }
                    default {
                        set data($column,anchor) left                                                   ;# and others left justified
                    }
                }
                lappend columns $column
            }
            lappend viewsColumns $columns
        }
        foreach columns $viewsColumns {
            lappend data(views) [list visibleColumns $columns swap 1]       ;# use a swapped display since data table has only 1 row
        }
    }

    proc processTableIdentifiers {table format trim} {
        # format is identifier,identifier,...,identifier,,identifier,identifier,... with , to separate columns and ,, tables
        variable data
        variable requestIdentifiers
        variable requestLength
        variable indexLength
        variable indexColumns                            ;# correspondance of identifier index (in the request) to displayed columns
        variable requestOids
        variable counter
        variable accessible                                                      ;# whether the table index as a whole is accessible

        # check that this is a proper table
        validateIdentifier $table
        set indexes [mib -exact index $table]
        if {$::tnm3} {
            set list {}
            foreach identifier $indexes {
                lappend list [mib label $identifier]                                                    ;# use readable name not oid
            }
            set indexes $list
        } elseif {([llength $indexes] == 1) && ([mib syntax $indexes] eq "SEQUENCE")} {
            # if table is an extension, Tnm 2 returns the entry as index, so go one level deeper to get at the index(es)
            set indexes [mib -exact index $indexes]
        }
        set indexLength [llength $indexes]
        if {($indexLength == 0) || ([mib syntax $table] ne "SEQUENCE OF")} {
            reportError $table "$table: not a table"                                       ;# must have an index but not be an entry
        }
        if {$::tnm3} {set command children} else {set command successor}
        set entry [mib -exact $command $table]
        if {[llength $entry] > 1} {
            reportError $table "$table has several successors: please report case to jfontain@free.fr"
        }

        # see whether table index is accessible as a whole
        set accessible 1
        foreach identifier $indexes {
            if {[mib access $identifier] eq "not-accessible"} {
                set accessible 0
                break
            }
        }
        set children [mib -exact $command $entry]                         ;# columns (including indexes) are the successors of entry
        # include indexes initialization, which may not be part of children when table is an extension
        foreach identifier [concat $indexes $children] {
            if {$::tnm3} {set identifier [mib label $identifier]}                                               ;# remove MIB header
            set notAccessible($identifier) [string equal [mib access $identifier] not-accessible]
        }
        if {!$accessible} {                      ;# in any index column is not accessible, consider all index columns not accessible
            foreach identifier $indexes {
                set notAccessible($identifier) 1
            }
        }

        # generate view(s) in the form of list(s) of object identifiers and internally generated columns
        if {$format eq ""} {                                                           ;# no identifiers format: display all columns
            # process index(es) first
            if {$indexLength > 1} {                                                                         ;# multiple column index
                # include a row number column so that, for example, different views can be identically sorted
                set identifiers (number)
                if {$accessible} {
                    eval lappend identifiers $indexes
                }
            } else {
                set identifiers $indexes    ;# always include index column first (if not accessible, index is first object instance)
            }
            # process remaining identifiers
            foreach identifier $children {                                                               ;# table column identifiers
                if {$::tnm3} {set identifier [mib label $identifier]}                                           ;# remove MIB header
                if {[lsearch -exact $identifiers $identifier] >= 0} continue                          ;# already in identifiers list
                if {$notAccessible($identifier)} continue
                lappend identifiers $identifier
            }
            set views [list $identifiers]                                                                             ;# single view
        } else {                                                                                           ;# generate list of views
            set views {}
            regsub -all ,, $format { } format                          ;# make multiple views format a list of comma separated views
            foreach list $format {
                if {$indexLength > 1} {                                                                     ;# multiple column index
                    # include a row number column so that, for example, different views can be identically sorted
                    set identifiers (number)
                } else {                                        ;# make sure single index column object is placed first in all views
                    set identifiers $indexes       ;# include index column first (if not accessible, index is first object instance)
                }
                foreach identifier [split $list ,] {                                                            ;# check identifiers
                    if {[lsearch -exact $identifiers $identifier] >= 0} continue                      ;# already in identifiers list
                    validateIdentifier $identifier
                    if {($entry ne [mib -exact parent $identifier]) && ([lsearch -exact $indexes $identifier] < 0)} {
                        # note: index identifiers can belong to another table
                        reportError $identifier "$identifier is not a column of table $table"
                    }
                    if {$notAccessible($identifier)} continue                                ;# never display not accessible objects
                    lappend identifiers $identifier
                }
                lappend views $identifiers
            }
        }

        # generate representation table columns data (include all columns, as views are used to select actual displayed data)
        set column 0
        foreach identifiers $views {
            foreach identifier $identifiers {
                switch $identifier {
                    (number) {                                                                                     ;# special column
                        set data($column,label) {}
                        set data($column,type) integer
                        set data($column,message) {row creation order}
                    }
                    default {                                                                                   ;# normal identifier
                        regsub ^$trim $identifier {} data($column,label)               ;# trim string from left side of column title
                        set type [identifierType $identifier bits($identifier)]      ;# remember whether the identifier is a counter
                        set data($column,type) $type
                        set data($column,message) [identifierMessage $identifier $bits($identifier)]
                        # display numeric values centered and others left justified:
                        switch $type {
                            integer - real {}
                            default {set data($column,anchor) left}
                        }
                    }
                }
                lappend identifierColumns($identifier) $column
                incr column
            }
        }

        # generate request object identifiers and their mapping to displayed columns
        for {set index 0} {$index < $indexLength} {incr index} {
            set counter($index) 0                                                            ;# index identifiers cannot be counters
        }
        set requestIdentifiers {}
        set requestOids {}
        set index 0
        set column 0
        catch {unset processed}
        foreach identifiers $views {
            foreach identifier $identifiers {
                if {[info exists processed($identifier)]} continue
                set processed($identifier) {}
                if {[string match (*) $identifier] || $notAccessible($identifier)} {
                    set indexColumns(key) $identifierColumns($identifier)
                } else {
                    lappend requestIdentifiers $identifier                                      ;# append identifier to request list
                    lappend requestOids [mib oid $identifier]
                    set indexColumns($index) $identifierColumns($identifier)
                    set counter($index) $bits($identifier)           ;# whether the identifier is a counter and how many bits it has
                    incr index
                }                                                            ;# else special column or not accessible: not requested
                incr column
            }
        }
        if {$index == 0} {
            error {no accessible columns}
        }
        set requestLength [incr index]                                                                    ;# including system uptime

        if {$format eq ""} {                                                                 ;# no views needed for whole table dump
            if {$indexLength == 1} {set data(sort) {0 increasing}}                           ;# single column index used for sorting
        } else {                        ;# generate view(s) containing visible columns, a subset of all representation table columns
            set column 0
            foreach identifiers $views {
                set columns {}
                foreach identifier $identifiers {
                    lappend columns $column
                    incr column
                }
                # always sort single index or number column
                lappend data(views) [list visibleColumns $columns sort [list [lindex $columns 0] increasing]]
            }
        }
    }

    proc identifierType {name counterName {exact 1}} {                                                 ;# must be a valid identifier
        upvar 1 $counterName counter

        set counter 0
        if {$exact} {
            set syntax [mib -exact syntax $name]
        } else {
            set syntax [mib syntax $name]
        }
        switch -glob [string tolower $syntax] {
            *gauge* - *integer* - *unsigned* {
                # (includes Gauge, Gauge32, INTEGER, Integer32 and Unsigned32)
                # note: integers can be enumerations, such as ifOperStatus, in which case they are converted to names by the SNMP
                # library, which used to create problems with viewers requiring numeric values, but which now can handle invalid
                # data, and furthermore, the user is unlikely to drop what looks like non numeric data in graph, pie, ... viewers
                return real
                # instead of integer for Tcl could not handle positive integers greater than 0x7FFFFFFF (for backward compatibility)
            }
            *counter64* {
                set counter 64                                                                                     ;# number of bits
                return real                                                            ;# since it is displayed as per second values
            }
            *counter* {                                                                          ;# (includes Counter and Counter32)
                set counter 32                                                                                     ;# number of bits
                return real                                                            ;# since it is displayed as per second values
            }
            default {
                return dictionary                             ;# (includes OCTET STRING, OBJECT IDENTIFIER, IpAddress and TimeTicks)
            }
        }
    }

    proc identifierMessage {name counter} {
        regsub -all {\r} [mib description $name] {} message                                           ;# clean up DOS formatted text
        if {$counter} {
            return "(per second for the last polling period)\n$message"
        } else {
            return $message
        }
    }

    proc update {} {
        variable requestIdentifiers
        variable indexLength
        variable busy
        variable bulkData
        variable bulkType

        if {[info exist busy]} return                                        ;# wait till request is complete before sending another
        set busy {}
        if {[info exists indexLength]} {                                                                                    ;# table
            catch {unset bulkData bulkType}                                                                  ;# reset responses data
            getBulk $requestIdentifiers
        } else {
            get $requestIdentifiers
        }
    }

    proc get {identifiers} {
        variable session
        variable trace

        set identifiers [concat 1.3.6.1.2.1.1.3.0 $identifiers]                                          ;# insert sysUpTime.0 first
        if {[info exists trace]} {
            puts ">>> request(get-request):[formattedIdentifiers $identifiers]"
        }
        $session get $identifiers {::snmp::processResponse %E %I [list %V]}
    }

    proc getBulk {identifiers} {
        variable session
        variable trace

        set identifiers [concat 1.3.6.1.2.1.1.3 $identifiers]                                              ;# insert sysUpTime first
        if {[info exists trace]} {
            puts ">>> request(get-bulk-request):[formattedIdentifiers $identifiers]"
        }
        $session getbulk 1 1 $identifiers {::snmp::processBulkResponse %E [list %V]}
    }

    proc formattedIdentifiers {list} {
        set string {}
        foreach identifier $list {
            append string " [mib name $identifier]"
        }
        return $string
    }

    proc formattedObjects {list} {                                                                               ;# list of varbinds
        set string {}
        foreach object $list {
            foreach {identifier type value} $object {}
            append string " [mib name $identifier]($value)"
        }
        return $string
    }

    proc secondsFromSystemUptime {object} {                                                      ;# (returns a floating-point value)
        foreach {identifier type value} $object {}
        if {$::tnm3} {                                                                                   ;# value is in centiseconds
            return [expr {$value / 100.0}]
        }
        scan $value {%ud %u:%u:%f} days hours minutes seconds
        return [expr {($days * 86400) + ($hours * 3600) + ($minutes * 60) + $seconds}]         ;# calculate system uptime in seconds
    }

    proc rate {value last period bits} {        ;# for counters (consider deltas greater than half full range too big hence invalid)
        if {$bits == 32} {                                                                                     ;# (most common case)
            set value [expr {($value - $last) & 0xFFFFFFFF}]            ;# 32 bit subtraction (valid on 32 and 64 bit Tcl platforms)
            if {$value & 0x80000000} {return ?}                              ;# negativity test (also works on 64 bit Tcl platforms)
        } else {                                                                                                          ;# 64 bits
            set value [expr {$value - $last}]                                                ;# (also works on 32 bit Tcl platforms)
            if {$value < 0} {return ?}                                                                         ;# too big (negative)
        }
        return [format %.2f [expr {$value / $period}]]                                                                 ;# per second
    }

    # objects are a list of varbinds (see snmp manual page) belonging to the same row
    proc processResponse {status errorIndex objects} {
        variable session
        variable indexColumns
        variable last
        variable data
        variable counter
        variable trace
        variable busy

        unset busy
        if {[info exists trace]} {
            puts "<<< response($status):[formattedObjects $objects]"
        }
        if {$status ne "noError"} {
            processError $status $errorIndex $objects
        }
        # objects list could be empty or system uptime value missing (may happen in "no such name" error cases):
        catch {set time [secondsFromSystemUptime [lindex $objects 0]]}
        if {![info exists time]} {
            for {set column 0} {1} {incr column} {                                                  ;# make data disappear from view
                if {[catch {unset data(0,$column)}]} break
            }
            catch {unset last(time)}
        } else {
            catch {set period [expr {$time - $last(time)}]}
            set objects [lrange $objects 1 end]                                                              ;# remove system uptime
            set index 0
            foreach object $objects {                                                                     ;# now fill row data cells
                foreach {identifier type value} $object {}
                if {$counter($index)} {
                    set new $value
                    if {[info exists period] && ($period > 0) && [info exists last($index)]} {
                        set value [rate $new $last($index) $period $counter($index)]
                    } else {
                        set value ?                                                   ;# need at least 2 values to make a difference
                    }
                    set last($index) $new
                } elseif {$type eq "OBJECT IDENTIFIER"} {
                    catch {set value [mib name $value]}                                      ;# attempt conversion to readable value
                    if {$::tnm3 && ([mib syntax $identifier] eq "OBJECT IDENTIFIER")} {
                        set value [mib label $value]                                                    ;# do not display MIB header
                    }
                } elseif {$type eq "TimeTicks"} {
                    set value [formattedTimeTicks $value]
                }
                set data(0,0) {}       ;# update hidden cell which may have been deleted if there was no returned data at least once
                foreach column $indexColumns($index) {
                    set data(0,$column) $value                                                                ;# there is only 1 row
                }
                incr index
            }
            set last(time) $time
        }
        incr data(updates)
    }

    proc processBulkResponse {status objects} {   ;# objects are a list of varbinds (see snmp manual page) belonging to the same row
        variable requestLength
        variable session
        variable requestOids
        variable data
        variable last
        variable trace
        variable busy
        variable bulkData
        variable bulkType

        if {[info exists trace]} {
            puts "<<< response($status):[formattedObjects $objects]"
        }
        set error [string compare $status noError]
        if {$error || ([llength $objects] != $requestLength)} {                         ;# number of responses differs from requests
            foreach name [array names data *,0] {                                                                   ;# total cleanup
                set row [lindex [split $name ,] 0]
                array unset data $row,\[0-9\]*
                array unset last $row,\[0-9\]*
            }
            incr data(updates)
            if {$error} {
                flashMessage "error: $status from [$session cget -address]"
            }
            unset busy
            return                                                                                                           ;# done
        }
        set time [secondsFromSystemUptime [lindex $objects 0]]
        set next 0                                                                      ;# whether to continue retrieving table data
        set identifiers {}
        set expression {\.(.*)$}
        set index 0
        foreach object [lrange $objects 1 end] requested $requestOids {                                        ;# skip system uptime
            catch {unset value}
            foreach {identifier type value} $object {}
            if {([string first $requested $identifier] == 0) && ($requested ne $identifier) && [info exists value]} {
                # oid is a successor but not equal to the request oid and object complete (for resiliency sake)
                regexp $expression [mib name $identifier] dummy key
                set bulkData($key,$index) [list $time $value]                 ;# store time and value corresponding to key and index
                if {![info exists bulkType($index)]} {set bulkType($index) $type}
                set next 1                                                   ;# keep retrieving data until all columns are completed
            }
            lappend identifiers $identifier
            incr index
        }
        if {$next} {                                                                ;# at least 1 column is not completely retrieved
            getBulk $identifiers                                                                     ;# keep going till end of table
        } else {                                                                                                             ;# done
            processBulkData $index
            incr data(updates)
            unset busy
        }
    }

    proc processBulkData {maximumIndex} {
        variable indexLength
        variable indexColumns
        variable last
        variable data
        variable counter
        variable accessible
        variable bulkData
        variable bulkType

        foreach name [array names bulkData] {                                                                 ;# determine data keys
            set keys([lindex [split $name ,] 0]) {}
        }
        foreach key [lsort -dictionary [array names keys]] {
            if {[catch {set row [hash64::numbers32 [split $key .] 1]} message]} {                                 ;# repeatable mode
                flashMessage "warning: duplicate row (key: $key) removed: $message"                           ;# should be very rare
                continue
            }
            if {![info exists data($row,0)]} {
                if {$indexLength > 1} {
                    set value [llength [array names data *,0]]
                    incr value                                                                  ;# creation order column starts at 1
                    foreach column $indexColumns(key) {set data($row,$column) $value}
                } elseif {!$accessible} {                                                      ;# not accessible single column index
                    foreach column $indexColumns(key) {set data($row,$column) $key}
                }
            }
            set current($row) {}                                                           ;# keep track of current rows for cleanup
            for {set index 0} {$index < $maximumIndex} {incr index} {
                if {[catch {foreach {time value} $bulkData($key,$index) {}}]} {             ;# missing cell: use void value for type
                    switch $data([lindex $indexColumns($index) 0],type) {
                        integer - real {set value ?}
                        default {set value {}}
                    }
                } elseif {$counter($index)} {
                    set new $value
                    if {\
                        ![catch {set period [expr {$time - $last($row,$index,time)}]}] && ($period > 0) &&\
                        [info exists last($row,$index)]\
                    } {
                        set value [rate $new $last($row,$index) $period $counter($index)]
                    } else {
                        set value ?                                                   ;# need at least 2 values to make a difference
                    }
                    set last($row,$index) $new
                    set last($row,$index,time) $time
                } elseif {$bulkType($index) eq "OBJECT IDENTIFIER"} {
                    catch {set value [mib name $value]}                                      ;# attempt conversion to readable value
                    if {$::tnm3 && ([mib syntax $identifier] eq "OBJECT IDENTIFIER")} {
                        set value [mib label $value]                                                    ;# do not display MIB header
                    }
                } elseif {$bulkType($index) eq "TimeTicks"} {
                    set value [formattedTimeTicks $value]
                }
                foreach column $indexColumns($index) {
                    set data($row,$column) $value                                            ;# fill the columns for that identifier
                }
            }
        }
        foreach name [array names data *,0] {                                                         ;# cleanup disappeared entries
            set row [lindex [split $name ,] 0]
            if {[info exists current($row)]} continue
            array unset data $row,\[0-9\]*
            array unset last $row,\[0-9\]*
        }
    }

    proc processError {status errorIndex objects} {
        variable session

        set message "error: $status from [$session cget -address]"
        if {$errorIndex > 0} {
            foreach {identifier type value} [lindex $objects [expr {$errorIndex - 1}]] {}                 ;# error index starts at 1
            if {[info exists identifier]} {
                append message " for [mib name $identifier] identifier"
            } else {
                append message " at index $errorIndex"
            }
        }
        flashMessage $message
    }

if {$::tnm3} {

    proc formattedTimeTicks {value} {                                                                    ;# value is in centiseconds
        set integer [expr {$value / 100}]
        return [formattedTime $integer][format %02u [expr {$value - ($integer * 100)}]]
    }

} else {

    proc formattedTimeTicks {value} {
        set centiseconds 0                                                                         ;# in case it is zero and not set
        scan $value {%ud %u:%u:%u.%u} days hours minutes seconds centiseconds
        return [format %ud%02uh%02um%02us%02u $days $hours $minutes $seconds $centiseconds]
    }

}

    proc mibFiles {directory names} {
        if {$directory ne ""} {
            if {![file isdirectory $directory]} {
                error "$directory: not a directory"
            }
            if {[llength $names] == 0} {                     ;# no specific MIBs specified: use all found in the specified directory
                set names [glob -nocomplain -directory $directory *.mib]
            }
        }
        set files {}
        foreach name $names {
            if {[file extension $name] eq ""} {
                append name .mib                                           ;# complete with default file name extension if necessary
            }
            if {($directory ne "") && ([file dirname $name] eq ".")} {
                set name [file join $directory $name]               ;# possibly join file without directory with specified directory
            }
            lappend files $name
        }
        return $files
    }

    proc terminate {} {
        variable session

        catch {$session destroy}                                                                ;# try to clean up and ignore errors
    }

}
