# # 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/>. # #//# # Create, manage, query and terminate user and background sessions. #//# package require comm 4.6.1 ;# Tcllib namespace eval session { namespace export close_session \ current_session \ user_sessions \ bg_sessions \ Session \ set_current_session \ is_user_session \ is_bg_session \ close_all_user_sessions \ close_all_bg_sessions \ listen_for_connections \ create_bg_session \ get_sessionId # The following variables store threadIds variable conSessions {} ;# List of active local console user session IDs variable vtySessions {} ;# List of active remote (VTY) user session IDs variable bgSessions {} ;# List of active background sessions IDs # Connection ID => Session ID variable CID2SID array set CID2SID {} ################################ # Connection Control ################################ proc new_message {id} { variable CID2SID if {[gets $id buffer] == -1} { if {[eof $id]} { close_session $::session::CID2SID($id) "Connection was lost with shell \"$id\"" return } return ;# fblocked probably } log::Debug "Received command from \"$id\":" $buffer if {[catch { switch -exact -- [lindex $buffer 0] { register { ;# "register" pid username type cols rows lassign [lrange $buffer 1 end] pid username type cols rows if {$type ne "console" && [llength $::session::vtySessions] >= [tsv::get conf SOFT_MAX_SESSIONS]} { log::Warning -error "Limit reached for concurrent remote sessions" } if {$type eq "console" && [llength $::session::conSessions] >= 1} { log::Warning -error "Only one console session is allows at any time" } set pid [lindex $buffer 1] set fifos [::session::register_new_fifos $pid] # Schedule create_user_session for execution at an idle time, so that we first return # (sending the response back), and then create_user_session is executed. after idle [list ::session::create_user_session $pid $id $fifos $username $type $cols $rows] log::Debug "Shell" $pid "Sending response: " $fifos puts $id $fifos } ping { puts $id pong } resize { ;# "resize" $COLS $ROWS set sid $::session::CID2SID($id) tsv::set $sid COLUMNS [lindex $buffer 1] tsv::set $sid ROWS [lindex $buffer 2] # No need to wait for the following. If there is a failure, we can do nothing about it. thread::send -async $sid ::pty::ptyresize thread::send -async $sid [list ::cli::handleAction RedrawLine] } default { log::Warning -error "Unknown command:" $buffer } } } errMsg]} { # Send to the other party the error, so it can print it on the terminal. puts $id $errMsg if {[info exists CID2SID($id)]} { close_session $CID2SID($id) $errMsg } else { close $id } } } proc new_connection {con} { log::Info "New connection with ID: $con" chan event $con readable [list ::session::new_message $con] fconfigure $con -blocking 0 -buffering line } if {0} {# Not working. proc listen_for_connections {} { Global SERVER_PORT log::Debug "Opening control socket: $SERVER_PORT" set fd [unix_sockets::listen $SERVER_PORT ::session::new_connection] fconfigure $fd -blocking 0 -buffering line } } # Monitor for incoming control commands. # We generate some errors, but mostly we let the errors generated by the procedures used herein # to propagate back to the caller (a shell or watchdog). # # @error proc listen_for_connections {} { Global SERVER_PORT log::Debug "Opening control socket" ::comm::comm config -port $SERVER_PORT -listen 1 -silent 0 :::comm::comm hook incoming { log::Debug "Incomming connection from ${addr}:${remport}" } :::comm::comm hook eval { set buffer [lindex $buffer 0] ;# Because the command comes like this: {{command}} log::Debug "Received command:" $buffer switch -exact -- [lindex $buffer 0] { register { ;# "register" pid username type cols rows lassign [lrange $buffer 1 end] pid username type cols rows if {$type ne "console" && [llength $::session::vtySessions] >= [tsv::get conf SOFT_MAX_SESSIONS]} { log::Warning -error "Limit reached for concurrent remote sessions" } if {$type eq "console" && [llength $::session::conSessions] >= 1} { log::Warning -error "Only one console session is allows at any time" } set pid [lindex $buffer 1] set fifos [::session::register_new_fifos $pid] # Schedule create_user_session for execution at an idle time, so that we first return # (sending the response back), and then create_user_session is executed. after idle [list ::session::create_user_session $pid $id $fifos $username $type $cols $rows] log::Debug "Shell" $pid "Sending response: " $fifos return $fifos } ping { return pong } resize { ;# "resize" $COLS $ROWS set sid $::session::CID2SID($id) tsv::set $sid COLUMNS [lindex $buffer 1] tsv::set $sid ROWS [lindex $buffer 2] # No need to wait for the following. If there is a failure, we can do nothing about it. thread::send -async $sid ::pty::ptyresize thread::send -async $sid [list ::cli::handleAction RedrawLine] return } default { log::Warning -error "Unknown command:" $buffer } } } :::comm::comm hook lost { return [close_session $::session::CID2SID($id) $reason] } } ################################ # Session creation ################################ # Assign a new pair of FIFOs for a new connection request for a User session. # # @param pid The pid of the requesting shell # @return A pair of assigned fifos to communicate with the shell. # @error proc register_new_fifos {pid} { Global FIFO_DIR log::Debug "Shell" $pid "Registering new FIFO pair" set fifoInFile [file join $FIFO_DIR in${pid}] set fifoOutFile [file join $FIFO_DIR out${pid}] catch { file delete $fifoInFile $fifoOutFile } log::Debug "Shell" $pid "Creating FIFO files" if {! [file exists $fifoInFile]} { ;# Just in case if {[catch { exec mkfifo -m 622 $fifoInFile ;# Shell has to write its stdin to this fifo } errMsg errStack]} { log::Error -error -stack $errStack "Could not create input FIFO: " $errMsg } } if {! [file exists $fifoOutFile]} { ;# Just in case if {[catch { exec mkfifo -m 644 $fifoOutFile ;# Shell has to read its stdout from this fifo } errMsg errStack]} { catch { file delete $fifoInFile } log::Error -error -stack $errStack "Could not create output FIFO: " $errMsg } } return [list $fifoInFile $fifoOutFile] } # Establish a connection with a shell and set up a new user session # # @assume authmode 'user authmode and confmode 'entry' exist. They should be created by 'base' module. # # @param pid The pid of the requesting shell. # @param cid The connection Id as returned by ::comm::comm self. # @param fifos A tuple: {input_fifo output_fifo} assigned to the shell. # @param type The type of user session. Supported types: "console" "vty" # @param username The name of the user connected to this session. # @param COLS Reported number of supported columns in the user terminal. # @param ROWS Reported number of supported rows in the user terminal. # @return The session Id of the newly created user session. # @error proc create_user_session {pid cid fifos username type COLS ROWS} { Global COLS_MAX COLS_MIN ROWS_MAX ROWS_MIN SESSION_TIMEOUT BASE_DIR FLOW_DEF MONITOR_DEF ANSI_DEF variable vtySessions variable conSessions variable CID2SID log::Info "New user session request for \"$username\" (pid: $pid) (interpreter: $cid)" lassign $fifos fifoInFile fifoOutFile if {[catch { ::helper::timeout $SESSION_TIMEOUT { log::Debug "Shell" $pid "Opening $fifoInFile channel for reading" set fifoIn [open $fifoInFile r] ;# can block. fconfigure $fifoIn -blocking 0 -buffering none -translation binary log::Debug "Shell" $pid "Opening $fifoOutFile channel for writing" set fifoOut [open $fifoOutFile w] ;# can block. fconfigure $fifoOut -blocking 1 -buffering none -translation binary log::Debug "Shell" $pid "Opening $fifoOutFile channel for writing second time" set fifoOut2 [open $fifoOutFile w] ;# will not block. fconfigure $fifoOut2 -blocking 1 -buffering none -translation binary } # Note: We have the following checks after FIFOs have been opened, so that we can print on the output FIFO # in order to provide feedback to the user. # Check for ROWS/COLS out of bounds if {! [string is integer $COLS] || $COLS > $COLS_MAX || $COLS < $COLS_MIN} { puts $fifoOut "The provided number of supported screen columns is either too large or too small." ;# Blocks log::Error -error "Shell" $pid "Number of columns out of bounds" } if {! [string is integer $ROWS] || $ROWS > $ROWS_MAX || $ROWS < $ROWS_MIN} { puts $fifoOut "The provided number of supported screen rows is either too large or too small." ;# Blocks log::Error -error "Shell" $pid "Number of rows out of bounds" } } errMsg errStack]} { # Either timeout or error. Do clean-up catch {close $fifoIn} catch {close $fifoOut} catch {close $fifoOut2} catch { file delete $fifoInFile $fifoOutFile } log::Error -error -stack $errStack "Shell" $pid "Failed to establish shell connection" $errMsg } # Creating new working thread. set sid [thread::create] thread::send $sid "set ptid [thread::id]" # Opening pipe to proxy Stdin. pipe pipe_r pipe_w fconfigure $pipe_r -blocking 0 -buffering none -translation binary fconfigure $pipe_w -blocking 0 -buffering none -translation binary # Transfer I/O to the thread thread::transfer $sid $pipe_r thread::transfer $sid $fifoOut # Establishing Stdin proxy. set ::session::special_$sid 0 ;# needed by ::session::proxy fileevent $fifoIn readable [list ::session::proxy $sid $fifoIn $pipe_w] # All state information for a session is confined within a unique shared memory array, # this allows for easy access from both the Session and the Master threads. # USERNAME : The unix account name for the user using the MikroConf shell and connected # to this session. # AUTHROOT : The AUTHMODE to use as the tree of all authentication modes, meaning that # the user cannot go on any of its ancestor AUTHMODEs with 'disable'. # This is the mode the user (or background) session enters to upon creation. # Must be set to something. No empty list. # CONFROOT : The CONFMODE to use as the tree of all configuration modes, meaning that # the user cannot go on any of its ancestor CONFMODEs with 'exit'. # This is the mode the user (or background) session enters to upon creation. # Must be set to something. No empty list. # POLICY : If set then this policy is enforced in all authentication modes, otherwise a session # that depends on the current AUTHMODE is enforced (or if not specified, the 'default' # policy is used). # SHELL_PID : The pid of the connecting shell # SHELL_CID : The connection id for this shell # USER : Flag to indicate that it is a user session (or not) # STDIN_W : The write part of the pipe where user input from the Master to the Session is redirected to. # STDIN_R : The channel that corresponds to the stdin FIFO. This is listened upon by Master and # redirected to Session. We need this Stdin redirection, in order for the Master to "see" # when the user presses the Ctlr ^ + x combination, to act accordingly. This is necessary # because if the Session event loop is blocked (e.g. from a user script in Slave), then # only master can sense this key combination and restart the blocked session. # FIFOINFILE : The full path for the fifo action as stdin # FIFOOUTFILE : The full path for the fifo action as stdout # STDIN : The channel where Session will listen for User input. This is the the read part of the # redirection pipe. # STDOUT : The channel that corresponds to the stdout FIFO # STDOUT2 : A second channel that corresponds to the same STDOUT FIFO. It is used by # Master to send async messages for this session, regardless of the state of the transfered # I/O to the Session interpreter. It is necessary because ::thread package allows us only to # transfer I/O to other threads, but not share. On the other hand we cannot send the logs for # printing of the event loop of the Slave, because Slave is allowed to block, and these messages # will not be printed asynchronously. In fact, sending the printing job to the Slave event loop, is # what we do when user asks for synchronous delivery of log/debug messages. # CREATED : Time this connection was established # ROWS : The number of rows supported by the terminal as reported by the MikroConf Shell # COLUMNS : The number of columns supported by the terminal as reported by the MikroConf Shell # INLINE : Reflects the state of the in-line scription functionality. Enabled/Disabled. It also # specifies the desired default setting (upon user login). # TRYCONNECT : Assume that unknown single-words commands are hostnames, and attempt to # connect to them via telnet. # FLOW : If set, flow control is enabled for text printed on the console. If it does not fit in a screen-full, # printing is paused and user can control the flow. Flow control is always dissabled for event # sessions. # MONITOR : Whether to print logs and debug messages on the terminal. If set, such messages are printed. # Otherwise they can only be views by inspecting the logs. # LAG : Time in ms to delay after a command is executed (any command or procedure). This safeguards # against accidental DoS attacks from the command line, by executing a script that consumes all the CPU. # Bare in mind that some commands internally execute other commands, hence the overal lag # is accumulated. For example "for" executes one time its first argument, and on every iteration # its second and third (which can in turn consist of many subcommands). # ANSI : Enable ANSI escape codes for colors, bold, underline, blinking etc.. tsv::set $sid USERNAME $username tsv::set $sid AUTHROOT {user} tsv::set $sid CONFROOT {entry} tsv::set $sid POLICY {} tsv::set $sid SHELL_PID $pid tsv::set $sid SHELL_CID $cid tsv::set $sid USER 1 tsv::set $sid STDIN_W $pipe_w tsv::set $sid STDIN_R $fifoIn tsv::set $sid FIFOINFILE $fifoInFile tsv::set $sid FIFOOUTFILE $fifoOutFile tsv::set $sid STDIN $pipe_r tsv::set $sid STDOUT $fifoOut tsv::set $sid STDOUT2 $fifoOut2 tsv::set $sid CREATED [clock seconds] tsv::set $sid ROWS $ROWS tsv::set $sid COLUMNS $COLS tsv::set $sid INLINE 0 ;# Disabled for user sessions by default. tsv::set $sid TRYCONNECT 1 tsv::set $sid FLOW $FLOW_DEF tsv::set $sid ANSI $ANSI_DEF tsv::set $sid MONITOR $MONITOR_DEF tsv::set $sid LAG 2 common_state $sid ;# Remaining variables # We use for Session Id the thread Id or the thread responsible for this session # thread.tcl can return an error if {[catch { thread::send $sid [list source [file join $BASE_DIR thread.tcl]] } errMsg errStack]} { # Any SLAVE interp, fileevents, and open descriptors will die automatically. catch {close $pipe_w} catch {close $fifoIn} catch {close $fifoOut2} catch { file delete $fifoInFile $fifoOutFile } thread::release $sid tsv::unset $sid log::Critical -error -stack $errStack "Shell" $pid "User session failed to start: " $errMsg } # Set initial root AUTHMODE and CONFMODE sysconf authmode $sid init sysconf confmode $sid init set CID2SID($cid) $sid switch -exact -- $type { console { lappend conSessions $sid } vty { lappend vtySessions $sid } default { error "Unsupported session type: $type" } } # Print User Banner Global USER_BANNER thread::send -async $sid [list puts -nonewline [? [lempty $USER_BANNER] "" "${USER_BANNER}\n"]] # Generated after a new user session has started. # Handy for printing a welcome message, logs, past connections, and other information. # # @param The session ID. # @param The unix account name of the just logged-in user. # @param The session type event generate USER_SESSION STARTED [list $sid $username $type] # Since this is a user session, load the command line interface. # We didn't include this in thread.tcl, because we want thread.tcl to be session-type neutral. thread::send $sid [list source [file join $BASE_DIR cli.tcl]] log::Debug "New user session created with session id \"$sid\" of type \"$type\"" return $sid } # Create a background session, which is event-driver and not user-driver. # # @assume authmode 'priv' and confmode 'global' exist. They should be created by 'base' module. # # @param username A symbolic name to distinguize this particular background session. By convention \ it should be in capitals, so that no user account can have the same name (unix user accounts can only \ have low case letters). # @param authmode The authmode to enter to, and the relative root of authmodes to set. Default is "priv" # @param confmode The confmode to enter to, and the relative root of confmodes to set. Default is "global" # @return The session Id of the newly created user session. proc create_bg_session {username {authmode {priv}} {confmode {global}}} { Global DEV_DIR DEFAULT_ROWS DEFAULT_COLS BASE_DIR PROC_DIR variable bgSessions log::Debug "New Background session request for \"$username\"" # Note: Until after setup_state is called, don't use 'log' or anything that uses our version of 'puts' # /dev/zero does not activate the readable handler when input is requested with 'read <chan>' as # opposed to 'read <chan> <num>', whereas /dev/null always terminates the handler with an EOF. set fdin [open [file join $DEV_DIR zero] r] fconfigure $fdin -blocking 0 -buffering line -translation binary # Note: We don't write output to a file, which we can monitor, because the user can generate # meaningless output that will be captured by this file and consume resources unecessarily. # Since the Event session is not interactive, output should not occur there at first place. Any # important information should be logged instead. set fdout [open [file join $DEV_DIR null] w] fconfigure $fdout -blocking 0 -buffering full -translation binary set sid [thread::create] thread::send $sid "set ptid [thread::id]" # Note: No limit for the number of parallel background sessions tsv::set $sid USERNAME $username tsv::set $sid AUTHROOT $authmode tsv::set $sid CONFROOT $confmode tsv::set $sid POLICY {bg} ;# special policy for event sessions tsv::set $sid USER 0 tsv::set $sid FIFOINFILE [file join $DEV_DIR zero] tsv::set $sid FIFOOUTFILE [file join $DEV_DIR null] tsv::set $sid STDIN $fdin tsv::set $sid STDOUT $fdout tsv::set $sid CREATED [clock seconds] tsv::set $sid ROWS $DEFAULT_ROWS tsv::set $sid COLUMNS $DEFAULT_COLS tsv::set $sid INLINE 1 ;# This is necessary in order for registered procedures to be evaluated. tsv::set $sid TRYCONNECT 0 tsv::set $sid FLOW 0 tsv::set $sid MONITOR 0 tsv::set $sid LAG 0 ;# No lag for background sessions. tsv::set $sid ANSI 0 common_state $sid ;# Remaining variables # Transfer I/O to the thread thread::transfer $sid $fdin thread::transfer $sid $fdout # We use for Session Id the thread Id or the thread responsible for this session # thread.tcl can return an error if {[catch { thread::send $sid [list source [file join $BASE_DIR thread.tcl]] } rerrMsg errStack]} { # Any SLAVE interp, fileevents, and open descriptors will die automatically. catch {close $fdin} catch {close $fdout} thread::release $sid tsv::unset $sid log::Emergency -error -stack $errStack "Background session failed to start (session Id: $sid): " $rerrMsg } # Set initial root AUTHMODE and CONFMODE sysconf authmode $sid init sysconf confmode $sid init lappend bgSessions $sid # Note: We don't load cli.tcl in background sessions, as there is no user interaction. log::Debug "New background session created with session id: $sid" # Generated after a new background session has started. # This is helpful for executing session start-up actions for event sessions. # # @param The session ID. event generate BACKGROUND_SESSION STARTED $sid return $sid } # Set up a number of shared variables common for both 'user' and 'background' session types. # # @param sid Session id proc common_state {sid} { Global PROMPT_DEF LOGTHREAS_DEF HISTORY_DEF SYNCH_DEF # SID : The Session ID. We use this unique identification of a session for multiple purposes: # * It is the thread Id of the Session thread. # * The name of the Slave interpreter. # * The shared memory area reserved for a particular session. # * To identify the session as a whole. # SLAVE : The name a slave interpreter to handle evaluation of in-line Tcl. We use the same name as the Thread (or session) id # CONFMODE: The name for the current configuration mode the session is currently in. # CONFSTRING: Prompt to indicate the current CONFMODE; e.g (conf), (rip), (ospf) # CONFSTORE: A place to store information regarding the current confmode. e.g. store the interface name when in interface configuration mode. # AUTHMODE: The name for the current authentication mode the session is currently in. # AUTHSTRING: Prompt to indicate the current CONFMODE; e.g >, # # HISTORY: The history buffer where previous commands are stored in a list format. # HISTORY_SIZE: Max number of entries maintained in the history buffer. # HISTORY_LEVEL: Where browsing the history with UP/DOWN keys, store the reletive position within the history that we are. # zero coresponds to no browsing at all (the currently edited command). The number increases to show the first in the past, second etc. # HISTORY_ENTRIES: Number of entries stored in the history buffer at any moment. This increases with every unique command entered until it reaches the size of history buffer. # HIST_SUBSTITUTIONS : Whether to enable history substitutions: !! !n !prefix ^old^new # COMMANDS: The Tcl commands present on the slave at the time of its creation. This list includes the ALIASES below. # ALIASES: The Tcl commands on the slave that are aliases at the time of its creation. # HIDDEN: The Tcl commands on the slave that are hidden at the time of its creation. # PROMPT: How the system prompt should look like. The following elements are supported: # %% : The percent character # %u : The name of the user logged-in # %h : Local host name # %d : Local domain name # %p : The AUTHMODE character, such as > for userlevel and # for privileged level # %c : The CONFMODE string, such as "(conf)" or "(rip)" # %s : Space character # %t : Tab character # %n : New line # %D : Present date in %d/%m/%Y format # %T : Present time in %H:%M format # %M : The machine (hardware) type (uname) # %R : OS release (uname) # %N : OS name (uname) # %V : OS version (uname) # %U : Uptime in <days>D<hours>H format(sysinfo) # %L : Load average in <1min>,<5min>,<15min> format (sysinfo) # %P : Percentage of free RAM (sysinfo) # %W : Percentage of free swap (sysinfo) # %E : The list of users currently logged-on in the system, in <user1> <user2> ... format (who) # %S : The number of user sessions currenly active in the system (who) # Any other character except for space and tabs is copied literally to the prompt # SYNCH : If set, logs and debug messages are sent synchronously on the terminal screen; # otherwise they are sent asynchronously. No logs/messages are sent to background sessions. # OUTBUF_SIZE : When SYNCH is set, this is the number of last logs to remember, to be printed # after the prompt becomes available again. # BUSY : Boolean to indicate whether the prompt is available, or a command is under execution. # Used in conjuction with SYNCH above to buffer incomming messages. # LOGTHREAS : Log threashold per session. If not specified the LOGTHREAS default session # is used instead. Messages with an equal or higher severity than that specified in this # variable will be printed on the terminal. If set to 8, then no messages will be printed at all. # TERM : Boolean. Indicates (if set) that the thread/session is in the process of terminating. No commands # are executed. tsv::set $sid SID $sid tsv::set $sid SLAVE $sid tsv::set $sid CONFMODE {} tsv::set $sid CONFSTRING {} tsv::set $sid CONFSTORE {} tsv::set $sid AUTHMODE {} tsv::set $sid AUTHSTRING {} tsv::set $sid HISTORY {} tsv::set $sid HISTORY_SIZE $HISTORY_DEF tsv::set $sid HISTORY_LEVEL 0 tsv::set $sid HISTORY_ENTRIES 0 tsv::set $sid HIST_SUBSTITUTIONS 1 ;# (@magic-number) tsv::set $sid COMMANDS {} tsv::set $sid ALIASES {} tsv::set $sid HIDDEN {} tsv::set $sid PROMPT $PROMPT_DEF tsv::set $sid SYNCH $SYNCH_DEF tsv::set $sid OUTBUF_SIZE 100 ;# (@magic-number) tsv::set $sid BUSY 0 tsv::set $sid LOGTHREAS $LOGTHREAS_DEF tsv::set $sid TERM 0 } # Get list of sids of user sessions # # @return The sid of all the user sessions in a list proc user_sessions {} { variable vtySessions variable conSessions return [concat $vtySessions $conSessions] } # Get list of sids of background sessions # # @return The sid of all the user sessions in a list proc bg_sessions {} { variable bgSessions return $bgSessions } # Checks if a session is a user session or not. # # @param sid The sid to check if is an user session. # @return The sid of all the user sessions in a list proc is_user_session {sid} { variable vtySessions variable conSessions if {$sid in [concat $vtySessions $conSessions]} { return 1 } return 0 } # Checks if a session is an background session or not. # # @param sid The sid to check if is an background session. # @return The sid of all the user sessions in a list proc is_bg_session {sid} { variable bgSessions if {$sid in $bgSessions} { return 1 } return 0 } # Kill shell. # # @param shell_pid Shell's PID. proc kill_shell {shell_pid} { catch { kill SIGTERM $shell_pid kill SIGKILL $shell_pid } } # Proxy input from channel $from to channel $to, while inspecting forwarded data and taking actions # as necessary. This key sequences are recognized: # Ctrl ^ + x : Terminate any currently executed command. # Ctrl ^ + s : Resize terminal screen to correspond to the real terminal dimentions, if it is not does automatically. # Ctrl ^ + r : Reset terminal screen. Useful if screen is distorded due to output etc. # # If connection in either $from or $to is lost, the session $sid is closed automatically. # # @param sid Session Id. # @param from Channel to redirect input from. # @param to Channel to redirect output to. proc proxy {sid from to} { if {[eof $from]} { fileevent $from readable {} close_session $sid "Connection with user was lost" return } set data [read $from] if {[set ::session::special_$sid]} { switch -exact -- [string index $data 0] { \u0078 { ;# Ctrl + ^ and then 'x' # This key sequense should break any executed command. The cases where a command can block are: # 1. In PTY isloated sessions with "ptyexec" (pty.tcl) like for commands: ping, traceroute, more, ... # 2. When waiting for user input with procedure "ask" (thread.tcl). # 3. When a command handler has blocked (due to normal execution or in runaway state), # and the "blocks" (helper.tcl) procedure has been used to make it non-blocking. Example of this # kind of block is waiting for a network connection to be establish, or waiting on a blocking read. # 4. When the event loop of the Session is occupied executing something or is in a runaway state # (e.g. due to user action in Slave, such as a endless loop) (session.tcl). This case includes also # registered event handlers executing in Events Session. # 5. When the event loop of the Session is in a blocked blocked (e.g. due to user action in Slave, # such as a blocking read). This case is currenly not dealt with anywhere. Any idea how? XXX # This case includes also registered event handlers executing in Events Session. # 6. Serving a 'bgexec' invocation. # Case 1: thread::send -async $sid ::pty::ptykill # Case 6: set ::bgexec_status 1 # Case 3: ::helper::break_event $sid # Case 4: Session $sid BUSY if {$BUSY} { # We schedule a job in the current thread to cancel the currently executing event in $sid. # At the same time we send to $sid 's event loop an event to cancel the previously scheduled job. # If the job is canceled, then it means that the event loop of $sid is free, so the user's Ctrl + ^ + x is # not for this type of blocking. If however the job is not cancelled, then it is executed, forcing $sid # to cancel the currently executing event. set job [after 200 [list thread::cancel $sid "\nbreak sequence pressed"]] ;# (@magic-number) thread::send -async $sid [list thread::send -async [thread::id] [list after cancel $job]] } # Case 2: # Enforced in proc ask. # For this reason we let this key sequence to propagate. } \u0073 { ;# Ctrl + ^ and then 's' # Resize $sid terminal # It has to go via Shell, because the shell only can query the terminal window dimentions. Session $sid SHELL_PID kill SIGUSR2 $SHELL_PID } \u0072 { ;# Ctrl + ^ and then 'r' # Reset terminal # It has to go via Shell, because the shell only can execute the 'reset' command to reset its tty. Session $sid SHELL_PID kill SIGUSR1 $SHELL_PID } } set ::session::special_$sid 0 } switch -exact -- [string index $data 0] { \u001e { ;# Ctrl + ^ (Enter special mode) set ::session::special_$sid 1 } } if {[eof $to]} { fileevent $from readable {} # Probably child has died, or closed standard I/O close_session $sid "Connection with session was lost" return } puts -nonewline $to $data } # Terminate a session and release memory # # @param sid Session id # @param reason The reason for Session termination. proc close_session {sid {reason "Unspecified"}} { log::Info "Closing session $sid" "Reason: $reason" Session $sid USER Session -rw $sid TERM set TERM 1 if {$USER} { Session $sid STDIN_R fileevent $STDIN_R readable {} } ::helper::break_event $sid thread::send $sid ::pty::ptykill # Wait for some time for the Session thread to clean-up stat that we cannot clean-up hear. # This can block because the event loop might be blocked in Session interp or Slave interp. if {[catch { Thread::bgsend $sid cleanup -timeout 2000 ;# (@magic-number) } errMsg errStack]} { log::Error "Failed to properly clean-up session $sid: $errMsg" } # Warning: Do not release it again here, misteriously it doesn't get released at all then! if {$USER} { Session $sid FIFOINFILE FIFOOUTFILE SHELL_PID SHELL_CID STDOUT2 variable CID2SID unset CID2SID($SHELL_CID) catch {close $STDOUT2} ;# The rest get closed in the child thread, where they have been transfered #close $SHELL_CID :::comm::comm shutdown $SHELL_CID catch { file delete $FIFOINFILE $FIFOOUTFILE } variable vtySessions variable conSessions if {$sid in $vtySessions} { lremove vtySessions $sid } if {$sid in $conSessions} { lremove conSessions $sid } unset ::session::special_$sid after 1000 ::session::kill_shell $SHELL_PID ;# (@magic-number) } else { variable bgSessions lremove bgSessions $sid } tsv::unset $sid log::Debug "Session $sid closed" } # Close all user sessions proc close_all_user_sessions {} { variable vtySessions variable conSessions foreach sid [concat $vtySessions $conSessions] { close_session $sid "Closing all user sessions" } } # Close all background sessions proc close_all_bg_sessions {} { variable bgSessions foreach sid $bgSessions { close_session $sid "Closing all background sessions" } } # Finds the session Id of the session on behalf of the caller procedure. # Since the entry point into Master for every MikroConf command execution is # "::sysconf::execute", and this entry point uses the sid variable to store the # sessionId, we can retrieve the value of sid from within this procedure. # # @assume The entry point is ::sysconf::execute and its second argument is the session id. # # @return The session id # @error proc get_sessionId {} { # Although "sysconf execute" is executed as an esemble, in the execution stack it appears as ::sysconf::execute set entrypoint [info level 1] if {[lindex $entrypoint 0] eq {::sysconf::execute}} { return [lindex $entrypoint 1] } log::Error -error "This command can only be invoked from within a MikroConf module operating on behalf of a user session" } } ;# End of namespace