Overview | Index by: file name | procedure name | procedure call | annotation
thread.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/>.
#

#//#
# This is the entry point for every new MikroConf session.
#
#//#

# Note: Global variable 'ptid' contains the thread id of the parent (Master thread).

# Note: Any new thread doesn't inherit the loaded packages & available procedures on its parent.

# Note: Remember that working path is that of the parent thread.

# Note: Loading the same binary package (like Tclx) in a thread, that has already been loaded in Master thread, does
#       not introduce any additional memory utilization. The code is reused. Might apply to Tcl-coded packages. However
#       it does not apply to files sourced with "source" command.

package require unix 0.5

################################
# Initiallization
################################

set sid [thread::id]

set BASE_DIR [tsv::get conf BASE_DIR]
source [file join $BASE_DIR lib.tcl]
source [file join $BASE_DIR pty.tcl]
source [file join $BASE_DIR policy.tcl]
unset BASE_DIR

namespace import ::policy::scripting ::policy::policy ::pty::ptyexec

################################
# Remap I/O
################################

Session $sid STDIN STDOUT

# The following makes the file ChannelId provided as the second argument an alias to the first.
# It doesn't close either of the channels. Closing one closes the other as well, but still to free
# up memory you need to run close on both of them.

# dup2 - Makes two Tcl File Handles to point to the same file descriptor.
# It also copies all the file handle options (fconfigure).
# The name is misleading, in the sense that it does not use the dup/dup2
# system calls. (Unline TclX "dup" command).

dup2 $STDIN stdin
dup2 $STDOUT stdout
dup2 $STDOUT stderr

unset STDIN STDOUT

################################
# Wrappers
################################

# Wrappers for log reporting procedures so that they also report the originating session/threadid.
namespace eval log {
proc Debug {args} { ::unknown log::Debug -session $::sid {*}$args }
proc Info {args} { ::unknown log::Info -session $::sid {*}$args }
proc Notice {args} { ::unknown log::Notice -session $::sid {*}$args }
proc Warning {args} { ::unknown log::Warning -session $::sid {*}$args }
proc Error {args} { ::unknown log::Error -session $::sid {*}$args }
proc Critical {args} { ::unknown log::Critical -session $::sid {*}$args }
proc Alert {args} { ::unknown log::Alert -session $::sid {*}$args }
proc Emergency {args} { ::unknown log::Emergency -session $::sid {*}$args }
}

# This version of unknown looks for the unknown procedure on the
# Master thread and if found, executes it there and bring back the result.
# If it's not found in the Master thread, then the "unknown" of the master
# thread will be executed.
# Note that this overwrites the original unknown, which we don't need.
#
# @param args The regular arguments to unknown.
# @error
proc unknown {args} {
    global ptid
    
    # We let the result and the error propagate.
    Thread::bgsend $ptid $args
}

rename exit _exit

# Terminate session.
#
# @param reason Reason for termination.
proc exit {{reason "Unspecified"}} {
    global ptid sid
    Thread::bgsend $ptid [list ::session::close_session $sid $reason]
}

# Called on session termination to clean up memory etc.
proc cleanup {} {    
    global sid
    Session $sid SLAVE STDIN STDOUT
    
    interp delete $SLAVE
    
    catch {close $STDIN}
    catch {close $STDOUT}
    
    thread::release ;# Reference count should now reach 0.
}

################################
# Error handling in Session
################################

# Our own error handling procedure for the Master interpreter
#
# @param msg The error message to display
proc bgerror {msg} {
    global sid errorInfo errorCode
    
    # Note: We don't have to rename bgerror just in case another error occurs within bgerror because:
    #   If another Tcl error occurs within the bgerror command (for example, because no bgerror command
    #   has been  defined)  then  Tcl  reports  the  error itself by writing a message to stderr.
    #   If  several background errors accumulate before bgerror is invoked to process them, bgerror will
    #   be invoked once for each error, in the order they occurred.  However, if bgerror returns with a break
    #   exception, then any remaining errors are skipped without calling bgerror.
    #
    #   Informationally: This is not the case for "unknown". If an unknown command is executed from
    #   within "unknown", the "unknown" command is executed again recursively. And since the same
    #   code is executed to handle the situation, an endless loop can easily occur.
    
    # The following message should be displayed on the users' terminal (if this is a user session).
    puts stderr $msg
    log::Error -stack $errorInfo -code $errorCode "Background error in session $sid: $msg"
}

# Custom errorproc to handle background errors arrising from asynchronous script executions
# between threads. If we don't use one, then errors are printed to stderr, which we don't want.
# bgerror is executed as well, after this.
#
# @param threadId Thread Id.
# @param errorInfo Error info.
proc errorproc {threadId errorInfo} {
    puts stderr $msg
    log::Error -stack $stacktrace "Background error in thread $threadId: $errorInfo"
}

thread::errorproc errorproc

################################
# I/O Wrappers
################################

# A 'gets' wrapper to simulate the behavior of Tcl's gets command when operating on a nonblocking non-canonical
# channel, which is what stdin is in a Session thread.
#
# We have the following requirements from the implementation:
#   1. It doesn't set the channel in blocking mode, which would prevent ::session::session_close from closing the session.
#   2. When Ctrl + C is pressed, then it returns imediately an empty list.
#   3. When Ctrl + ^ and then x, is pressed then it terminates imediately returning an error to the caller which will cause
#       it to terminate as well.
#
# It only supports the one-line argument syntax of the original gets:
#       gets channelId
#
# Backspace can be used as in the original command. The following options are supported:
#        -noecho : The typed characters are not displayed back to the user. Appropriate for passwords. (default: echo characters)
#        -nonewline : Don't print a new line when the user presses enter to submit the string. (default: print newline)
#        -end <string> : Specifies a string that will signify the end of input. Defaults to newline. This option allow getting multi-line text.
#        -length <num> : Set a limit on the number of characters the user can enter. (default: no limit)
#
# @error
proc nbgets {args} {
    global LINE
    global nbgets_special
    global nbgets_done
    global nbgets_break
    global nbgets_chcount
    
    set LINE {}
    lassign {0 0 0 0} nbgets_special nbgets_done nbgets_break nbgets_chcount
    
    # Note: length is set to a hard-coded maximum string input length, that
    # serves for avoiding Memory DoS attacks.
    array set Param {
        noecho 0
        nonewline 0
        length 100000
        end "\n"
    }
    
    for {set i 0} {$i < [llength $args]} {incr i} {
        switch -exact -- [lindex $args $i] {
            "-noecho" { set Param(noecho) 1 }
            "-nonewline" { set Param(nonewline) 1 }
            "-end" { set Param(end) [lindex $args [incr i]] }
            "-length" { set Param(length) [lindex $args [incr i]] }
            default {
                break
            }
        }
    }
    set args [lrange $args $i end]
    
    # Acceptable syntax after this point:
    #   gets channelId ?varName?
    
    switch -exact -- [llength $args] {
        0 {
            set channelId stdin
        }
        1 {
            set channelId $args
        }
        2 {
            # This is necessary because we cannot use uplevel/upvar to set a variable on caller's
            # context, since the caller resides in a different interpreter or a different interpreter & thread.
            error "unlike Tcl's gets, this version of gets doesn't support the ?varName? parameter"
        }
        default {
            error "wrong # args: should be \"gets channelId\""
        }
    }

    if {$channelId ne "stdin"} {
        return [gets $channelId]
    }
    
    fileevent stdin readable [list nbgets_event $channelId [array get Param]]
    vwait ::nbgets_done
    
    if {$nbgets_break} {
        error "\nbreak sequence pressed"
    }
    
    return $LINE
}

# Fileevent handler to process and record user input.
#
# @param channel The channel user input is coming from.
# @param param A serialized array with the parameters supported by "nbgets".
proc nbgets_event {channel param} {
    global LINE
    global nbgets_special
    global nbgets_done
    global nbgets_break
    global nbgets_chcount
    array set Param $param
    set ESC "\033"
    
    if {[eof $channel]} {
        fileevent $channel readable {}
        exit "Connection lost while waiting for user input"
    }

    set char [read stdin 1]

    if {$nbgets_special} {
        switch -exact -- $char {
            \u0078 { ;# Ctrl + ^ and then 'x'
                fileevent stdin readable {}
                set LINE ""
                set nbgets_done 1
                set nbgets_break 1 ;# On espace sequence we generate error, so that 
                                            # the caller will halt its operations, and return imediately.
            }
            \u0073 { ;# Ctrl + ^ and then 's'
                # Resize terminal
                # Handled in session.tcl.
            }
            \u0072 { ;# Ctrl + ^ and then 'r'
                # Reset terminal
                # Handled in session.tcl.
            }
            default {
                # Consume $data. Do nothing.
            }
        }
        set nbgets_special 0
        return
    }
    
    switch -exact -- $char {
        \u0003 { ;# ^C break
            fileevent stdin readable {}
            set LINE "" ;# On espace sequence we return nothing, so that the caller will continue its operations.
                                 # Caller cannot distinguise between user entered nothing, and the espace sequence.
            puts "" ;# newline to prettify output.
            set nbgets_done 1
        }
        \u007f { ;# backspace
            if {! $Param(noecho) && $nbgets_chcount > 0} {
                puts -nonewline "$ESC\[D"
                puts -nonewline " "
                puts -nonewline "$ESC\[D"
                incr nbgets_chcount -1
            }
            set LINE [string range $LINE 0 end-1]
        }
        \u001e { ;# Ctrl + ^ (Enter special mode)
            set nbgets_special 1
        }
        default {
            if {$char eq "\r"} { set char "\n" }
            if {[string is print $char] || $char eq "\n"} {
                set index [expr {[string length $Param(end)]-1}]
                append LINE $char
                if {[string range $LINE end-$index end] eq $Param(end)} {
                    set LINE [string range $LINE 0 end-[string length $Param(end)]]
                    if {! $Param(nonewline)} {
                        puts ""
                    }
                    fileevent stdin readable {}
                    set nbgets_done 1
                    return
                }
                if {[string length $LINE] >= $Param(length)} {
                    puts -nonewline "\u7"
                    set LINE [string range $LINE 0 end-1]
                    continue
                }
                if {! $Param(noecho)} {
                    puts -nonewline $char
                }
                incr nbgets_chcount
            } else {
                puts -nonewline "\u7"
            }
        }
    }
}

# Like 'puts' but automatically switches to --more-- functionality when the string to be printed is going 
# to take more lines than those available on the remote terminal screen. The follow prerequisites
# must also be fulfilled for this to happen:
#   * The output channel is stdout.
#   * Flow control is enabled.
# We outsource the --more-- functionalily from the external 'more' busybox utility.
#
# @assume The syntax of the command will not change.
# @param args The normal arguments for the 'puts' command.
# @error
proc more {args} {
    global sid
    Session $sid COLUMNS ROWS FLOW
    
    set channelId stdout
    switch -exact -- [llength $args] {
        1 { ;# puts string
            lassign $args txt
            append txt \n
        }
        2 {
            if {[lindex $args 0] eq "-nonewline"} { ;# puts -nonewline string
                lassign $args {} txt
            } else { ;# puts channelId string
                lassign $args channelId txt
                append txt \n
            }
        }
        3 { ;# puts -nonewline channelId string
            if {[lindex $args 0] eq "-nonewline"} { ;# puts -nonewline string
                lassign $args {} channelId txt
            } else {
                error "unrecognized option [lindex $args 0]"
            }
        }
        default {
            error "wrong # args: should be \"puts ?-nonewline? ?channelId? string\""
        }
    }
    
    # The easy checks go first, for efficiency. The next checks are more computation intensive.
    if {$channelId ne "stdout" || ! $FLOW} {
        puts -nonewline $channelId $txt
        return
    }
        
    # Calculate the number of lines that the command will take
    set total 0
    foreach line [split $txt "\n"] {
        incr total [expr {     int(     ceil(    double([string length $line]) / $COLUMNS     )    )     }]
    }
    
    # If output does not fit in a screenful, we use --more--
    # Note: We cannot use Memchan to store the output, because it only has effect on Tcl code
    # (not external apps), so we use a regular file.
    if {$total > $ROWS} {
        global sid
        Global WORK_DIR
        set fd [open [file join $WORK_DIR $sid] w] ;# let the error propagate
        puts -nonewline $fd $txt
        close $fd ;# let the error propagate
        unset txt
        if {[catch {
            ptyexec more [file join $WORK_DIR $sid]
        } errMsg errStack]} {
            catch { file delete [file join $WORK_DIR $sid] }
            log::Error -error -stack $errStack "Failed to execute more: " $errMsg
        }
        catch { file delete [file join $WORK_DIR $sid] }
    } else {
        puts -nonewline $channelId $txt
    }
    return
}

################################
# Buffering for synchronous output
################################

set outBuffer {}

# If prompt is busy executing some command, buffer incoming messages for printing.
# Otherwise print at once.
#
# @param txt The message to be printed.
proc pushbuf {txt} {
    global sid outBuffer
    Session $sid OUTBUF_SIZE BUSY
    
    if {! $BUSY} {
        puts $txt
    } else {        
        if {[llength $outBuffer] == $OUTBUF_SIZE} {
            set outBuffer [lrange $outBuffer 1 end]
        }
        lappend outBuffer $txt
    }
}

# Retrieve pending messages to be printed.
#
# @param num The number of messages to retrieve (the oldest ones). If {} then all messages are retrieved.
# @return A list of pending messages in chronological order from oldest to most recent.
proc popbuf {{num "all"}} {
    global outBuffer
    
    if {$num eq "all"} {
        set num [llength $outBuffer]
    }
    
    set buf [lrange $outBuffer 0 $num-1]
    set outBuffer [lrange $num-1 end]
    return $buf
}

# Retrieve the number of pending messages.
#
# @return The number of pending messages.
proc bufsize {} {
    global outBuffer
    return [llength $outBuffer]
}

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