# 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: system.tcl,v 1.25 2006/01/28 19:16:59 jfontain Exp $


package provide system [lindex {$Revision: 1.25 $} 1]
package require network 1
package require miscellaneous 1
package require stooop 4.1
namespace import stooop::*
package require switched
if {[catch {package require Thread 2.5}]} {
    namespace eval system {variable threads 0}
} else {                                                                                  ;# load thread worker class implementation
    package require threads 1
    namespace eval system {variable threads 1}
}
package require linetask 1


namespace eval system {

    array set data {
        updates 0
        0,label version 0,type ascii 0,message {kernel version}
        1,label date 1,type clock 1,message {kernel build date}
        2,label time 2,type clock 2,message {kernel build time}
        3,label {CPU vendor} 3,type ascii 3,message {processor vendor identification}
        4,label {CPU model} 4,type ascii 4,message {processor model name}
        5,label {CPU speed} 5,type real 5,message {processor speed in megahertz}
        6,label {CPU MIPS} 6,type real 6,message {processor speed in bogomips}
        7,label {up time} 7,type dictionary 7,message {system uptime in d(ays), h(ours), m(inutes) and s(econds)}
        8,label {idle time} 8,type dictionary 8,message {system idle time in d(ays), h(ours), m(inutes) and s(econds)}
        9,label users 9,type integer 9,message {number of users currently logged on}
        10,label processes 10,type integer 10,message {number of processes}
        pollTimes {60 10 20 30 120 300 600}
        views {{indices {0 1 2 3 4 5 6 7 8 9 10} swap 1}}
        persistent 1
        switches {-C 0 --daemon 0 -i 1 -p 1 -r 1 --remote 1}
    }
    set file [open system.htm]
    set data(helpText) [::read $file]                                                         ;# initialize HTML help data from file
    close $file
    unset file

    proc initialize {optionsName} {
        upvar 1 $optionsName options
        variable data
        variable local
        variable remote
        variable threads                                                                     ;# whether threads package is available

        catch {set locator $options(-r)}
        catch {set locator $options(--remote)}                                                                  ;# favor long option
        if {![info exists locator]} {                                                                                  ;# local host
            set local(release) [open /proc/sys/kernel/osrelease]                     ;# keep local files open for better performance
            set local(version) [open /proc/sys/kernel/version]
            set local(uptime) [open /proc/uptime]
            set local(load) [open /proc/loadavg]
            set local(cpu) [open /proc/cpuinfo]
            return                                                                                               ;# local monitoring
        }
        foreach {remote(protocol) remote(user) remote(host)} [network::parseRemoteLocator $locator] {}
        set remote(rsh) [string equal $remote(protocol) rsh]
        set data(identifier) system($remote(host))
        # important: pack data in a single line using special control separator characters
        set remote(command) {cat /proc/sys/kernel/osrelease /proc/sys/kernel/version /proc/uptime /proc/loadavg /proc/cpuinfo 2>&1 | tr '\n' '\v'}
        if {[string equal $::tcl_platform(platform) unix]} {
            if {$remote(rsh)} {
                set command "rsh -n -l $remote(user) $remote(host) {$remote(command)}"
            } else {
                set command ssh
                if {[info exists options(-C)]} {append command { -C}}                                            ;# data compression
                if {[info exists options(-i)]} {append command " -i \"$options(-i)\""}                              ;# identity file
                if {[info exists options(-p)]} {append command " -p $options(-p)"}                                           ;# port
                append command " -T -l $remote(user) $remote(host)"
            }
        } else {                                                                                                          ;# windows
            if {$remote(rsh)} {
                error {use -r(--remote) ssh://session syntax (see help)}
            }
            set remote(rsh) 0
            set command "plink -ssh -batch -T $remote(host)"       ;# note: host must be a putty session and pageant must be running
        }
        if {$remote(rsh)} {
            set access r                                                                            ;# writing to pipe is not needed
        } else {
            set access r+                                                                                     ;# bi-directional pipe
            # terminate remote command output by a newline so that the buffered stream flushes it through the pipe as soon as the
            # remote data becomes available:
            append remote(command) {; echo}
        }
        set remote(task) [new lineTask\
            -command $command -callback system::read -begin 0 -access $access -translation lf -threaded $threads\
        ]
        if {![info exists options(--daemon)] && !$remote(rsh)} {             ;# for ssh, detect errors early when not in daemon mode
            lineTask::begin $remote(task)
        }                                                       ;# note: for rsh, shell and command need be restarted at each update
        set remote(busy) 0
    }

    proc update {} {
        variable remote
        variable local

        if {[info exists remote]} {
            if {$remote(busy)} return                                               ;# core invocation while waiting for remote data
            set remote(busy) 1
            if {[lineTask::end $remote(task)]} {                                                           ;# rsh or ssh daemon mode
                lineTask::begin $remote(task)                       ;# note: for rsh, shell and command are restarted here each time
            }
            if {!$remote(rsh)} {
                lineTask::write $remote(task) $remote(command)             ;# start data retrieval by sending command to remote side
            }
        } else {
            seek $local(release) 0                                                                  ;# rewind before retrieving data
            seek $local(version) 0
            seek $local(uptime) 0
            seek $local(load) 0
            seek $local(cpu) 0
            process [split [::read $local(release)][::read $local(version)][::read $local(uptime)][::read $local(load)][::read -nonewline $local(cpu)] \n]
        }
    }

    proc process {lines} {
        variable data

        # data is wholly updated at each poll, so reset each time in case of error:
        array set data {0,0 {} 0,1 {} 0,2 {} 0,3 {} 0,4 {} 0,5 ? 0,6 ? 0,7 {} 0,8 {} 0,9 ? 0,10 ?}       ;# use ? (void) for numbers
        if {![regexp {^[\d\.]+} [lindex $lines 0]]} {                              ;# ignore extra characters, such as in 2.2.0-pre1
            set message "invalid data: [lindex $lines 0]"
            if {[llength $lines] > 1} {append message "..."}
            flashMessage $message
            incr data(updates)
            return
        }
        set index 0
        foreach line $lines {
            switch $index {
                0 {                                                                                                  ;# release file
                    set data(0,0) $line
                }
                1 {                ;# version file: ignore heading (#n, SMP, ...) and day. example : #1 Fri Oct 6 22:01:22 CEST 2000
                    set data(0,1) "[lrange $line end-4 end-3], [lindex $line end]"                                ;# month day, year
                    set data(0,2) [lindex $line end-2]                                                                   ;# HH:MM:SS
                }
                2 {                                                                                                   ;# uptime file
                    set data(0,7) [formattedTime [expr {round([lindex $line 0])}]]
                    set data(0,8) [formattedTime [expr {round([lindex $line 1])}]]
                }
                3 {                                                                                                     ;# load file
                    scan $line {%*f %*f %*f %u/%u} data(0,9) data(0,10)
                }
                default {                                                                                                ;# cpu file
                    if {![regexp {^(.+?)\s+:\s+(.+?)$} $line dummy variable value]} continue
                    switch $variable {
                        vendor_id {set data(0,3) $value}
                        {model name} {set data(0,4) $value}
                        {cpu MHz} {set data(0,5) $value}
                        bogomips {set data(0,6) $value}
                    }
                }
            }
            incr index
        }
        incr data(updates)
    }

    proc read {line} {                                       ;# read remote data now that it is available and possibly handle errors
        variable remote

        switch $lineTask::($remote(task),event) {
            end {
                # either valid data availability as rsh connection was closed, or connection broken for ssh, in which case remote
                # shell command will be attempted to be restarted at next update
            }
            error {                                                                              ;# some communication error occured
                set message "error on remote data: $lineTask::($remote(task),error)"
            }
            timeout {                                                                         ;# remote host did not respond in time
                set message "timeout on remote host: $remote(host)"
            }
        }
        # note: in case of an unexpected event, task insures that line is empty
        if {[info exists message]} {
            flashMessage $message
        }
        process [split [string trimright $line \v] \v]
        set remote(busy) 0
    }

}
