I need to test how my remote server is handling ping request. I need to ping remote server from my windows with payload of say 50 kb. I need my tcl script sholud generate 20 such ping requests with 50 kb payload parallel so it will result 1 mb receive traffic at server at given instance. here is the code for ping test
proc ping-igp {} {
foreach i {
172.35.122.18
} {
if {[catch {exec ping $i -n 1 -l 10000} result]} {
set result 0
}
if {[regexp "Reply from $i" $result]} {
puts "$i pinged"
} else {
puts "$i Failed"
}
}
}
If you want to ping in parallel then you can use open instead of exec and use fileevents to read from the ping process.
An example of using open to ping a server with two parallel processes:
set server 172.35.122.18
proc pingResult {chan serv i} {
set reply [read $chan]
if {[eof $chan]} {
close $chan
}
if {[regexp "Reply from $serv" $result]} {
puts "$serv number $i pinged"
} else {
puts "$serv number $i Failed"
}
}
for {set x 0} {$x < 2} {incr $x} {
set chan [open "|ping $server -n 1 -l 10000"]
fileevent $chan readable "pingResult $chan {$server} $x"
}
See this page for more info: http://www.tcl.tk/man/tcl/tutorial/Tcl26.html
Here's a very simple piece of code to do the pinging in the background by opening a pipeline. To do that, make the first character of the “filename” to open be a |, when the rest of the “filename” is interpreted as a Tcl list of command line arguments, just as in exec:
proc doPing {host} {
# These are the right sort of arguments to ping for OSX.
set f [open "|ping -c 1 -t 5 $host"]
fconfigure $f -blocking 0
fileevent $f readable "doRead $host $f"
}
proc doRead {host f} {
global replies
if {[gets $f line] >= 0} {
if {[regexp "Reply from $host" $result]} {
# Switch to drain mode so this pipe will get cleaned up
fileevent $f readable "doDrain $f"
lappend replies($host) 1
incr replies(*)
}
} elseif {[eof $f]} {
# Pipe closed, search term not present
lappend replies($host) 0
incr replies(*)
close $f
}
}
# Just reads and forgets lines until EOF
proc doDrain {f} {
gets $f
if {[eof $f]} {close $f}
}
You'll also need to run the event loop; this might be trivial (you're using Tk) or might need to be explicit (vwait) but can't be integrated into the above. But you can use a clever trick to run the event loop for long enough to collect all the results:
set hosts 172.35.122.18
set replies(*)
foreach host $hosts {
for {set i 0} {$i < 20} {incr i} {
doPing $host
incr expectedCount
}
}
while {$replies(*) < $expectedCount} {
vwait replies(*)
}
Then, just look at the contents of the replies array to get a summary of what happened.
Related
I am currently encountering a problem.
When I want to download a file on a mikrotik in 6.48.6 using mtlogin and fetch tool, it works perfectly and the script waits until the router has finished downloading to send a "quit".
However, when trying the same manipulation on a router in version 7.1.5, the "quit" is sent directly, thus stopping the download because of the letter Q and thus sending "uit" thereafter in the prompt.
The prompts are similar for 6.48.6 and 7.1.5, and even when trying to add expects in the script, the result is the same.
I think the problem is in this part of the code, but don't know how to fix it.
# Run commands given on the command line.
proc run_commands { prompt command } {
global do_interact in_proc
set in_proc 1
# escape any parens in the prompt, such as "(enable)"
regsub -all "\[)(]" $prompt {\\&} reprompt
# handle escaped ;s in commands, and ;; and ^;
regsub -all {([^\\]);} $command "\\1\u0002;" esccommand
regsub -all {([^\\]);;} $esccommand "\\1;\u0002;" command
regsub {^;} $command "\u0002;" esccommand
regsub -all {[\\];} $esccommand ";" command
regsub -all {\u0002;} $command "\u0002" esccommand
set sep "\u0002"
set commands [split $esccommand $sep]
set num_commands [llength $commands]
for {set i 0} {$i < $num_commands} { incr i} {
send -- "[subst -nocommands [lindex $commands $i]]\r"
if { [lindex $commands $i] == "/system/reboot"} {
send "y\r"
}
expect {
-re "^\[^\n\r]*$reprompt" {}
-re "^\[^\n\r ]*>>.*$reprompt" { exp_continue }
-re "\[\n\r]+" { exp_continue }
}
}
if { $do_interact == 1 } {
interact
return 0
}
send "quit\r"
expect {
-re "^WARNING: There are unsaved configuration changes." {
send "y\r"
exp_continue
}
"\n" { exp_continue }
"\[^\n\r *]*Session terminated" { return 0 }
timeout { catch {close}; catch {wait};
return 0
}
eof { return 0 }
}
set in_proc 0
}
That's how it looks like
Does anyone have a solution?
I just find the solution in mtlogin at line 625!
foreach router [lrange $argv $i end] {
set router [string tolower $router]
send_user "$router\n"
# Figure out prompt.
set prompt "] > " #Just added a second whitespace after >
# alteon only "enables" based on the password used at login time
set autoenable 1
set enable 0
Hope it's gonna help you
I have tried the following:
set timeout -1
spawn $env(SHELL)
match_max 100000
send -- "ssh somewhere#addr"
expect "*"
for { set i 0 } { $i < [array size env] } { incr i } {
send -- "echo env($i) = $env($i)"
expect "*"
}
send -- "env > foo\r"
expect "*"
send -- "^D"
expect eof
The above fails with the following error:
no such variable
(read trace on "::env(0)")
invoked from within
"send -- "echo env($i) = env($i)""
("for" body line 2)
invoked from within
"for { set i 0 } { $i < [array size env] } { incr i } {
send -- "$::env($i)"
expect "*"
}"
(file "./script.exp" line 52)
I'll get around system issues by re-sourcing the rc files; I merely want to transport an unknown collection of vars (relative to the script) set by me to the remote execution environment.
Is this possible, I am unfamiliar with TCL (total beginner)?
Tcl arrays are more like a dictionary of keys and values than a list indexed by number. You can get the list of keys with [array names env] and traverse them with foreach:
foreach n [array names env] {
send -- "echo env($n) = $env($n)\r"
expect "\n"
}
Is it possible to spawn an application, send commands, expect results, but also 'respawn' the application in case it crashed and continue from the last point? I tried creating procedures for spawning, but I am not able to catch user shell prompt once the application gets closed
So it sounds like you are doing something like telneting or sshing
into a shell then running an application. The application dies or
hangs so the prompt does not return so expect does not return and so
you are stuck. You will need to use timeout to detect a hang and restart
your entire process ( including the spawn). Below is the boiler plate I start with when writing
expect scripts. The critical thing to help you resolve your problem
is to realize that spawn not only sets the spawn_id but also returns
the pid of the spawned process. You can use that pid to kill the
spawned process if you get an eof and/or a timeout. My boiler plate
kills the spawn process on timeout. The timeout does not bail from the
expect loop but waits for the eof before exiting. it also accumulates
the output of expect command so on timeout you may be able to see
where it dies. The boilerplate is in a proc called run. Spawn the process
and pass the pid and the spawn id . Reuse the boiler plate to define other
procs that will be the steps. put the procs in a script with a counter between
them as shown and repeat. The other answerer is correct that the unless the app
restarts where you left off you can need to start from scratch. If it does start from
where you left off. make the steps granular enough that you know what command
to repeat. and step to start from.
BoilerPlate
proc run { pid spawn_id buf } {
upvar $buf buffer; #buffer to accumulate output of expect
set bad 0;
set done 0;
exp_internal 0; # set to one for extensive debug
log_user 0; # set to one to watch action
expect {
-i $spawn_id
-re {} {
append buffer $expect_out(buffer); # accumultate expect output
exp_continue;
}
timeout {
send_user "timeout\n"
append buffer $expect_out(buffer); # accumultate expect output
exec kill -9 $pid
set bad 1
exp_continue;
}
fullbuffer {
send_user " buffer is full\n"
append buffer $expect_out(buffer); # accumultate expect output
exp_continue;
}
eof {
send_user "Eof detected\n"
append buffer $expect_out(buffer); # accumultate expect output
set done 1 ;
}
}
set exitstatus [ exp_wait -i $spawn_id ];
catch { exp_close -i $spawn_id };
if { $bad } {
if { $done } {
throw EXP_TIMEOUT "Application timeout"
}
throw BAD_ERROR "unexpected failure "
}
return $exitstatus
}
set count 0
set attempts 0 ; # try 4 times
while { $count == 0 && $attempts < 4 } {
set buff ""
set pid [spawn -noecho ssh user#host ]
try {
run $pid $::spawn_id buff
incr count
run2 $pid $::spawn_id buff
incr count
run3 $pid $::spawn_id buff
incr count
run4 $pid $::spawn_id buff
incr count
} trap EXP_TIMEOUT { a b } {
puts "$a $b"
puts " program failed at step $count"
} on error { a b } {
puts "$a $b"
puts " program failed at step $count"
} finally {
if { $count == 4 } {
puts "success"
} else {
set count 0
incr attempts
puts "$buff"
puts "restarting\n"
}
}
}
tclsh is a shell containing the TCL commands.
The TCL uplevel command evaluates the given TCL script, but it fails to evaluate a tclsh script (which can contain bash commands).
How can I obtain an analogue of uplevel for the tclsh script?
Consider this TCL script:
# file main.tcl
proc prompt { } \
{
puts -nonewline stdout "MyShell > "
flush stdout
}
proc process { } \
{
catch { uplevel #0 [gets stdin] } got
if { $got ne "" } {
puts stderr $got
flush stderr
}
prompt
}
fileevent stdin readable process
prompt
while { true } { update; after 100 }
This is a kind of TCL shell, so when you type tclsh main.tcl it shows a prompt MyShell > and it acts like you are in interactive tclsh session. However, you are in non-interactive tclsh session, and everything you type is evaluated by the uplevel command. So here you can't use bash commands like you can do it int interactive tclsh session. E.g. you can't open vim right from the shell, also exec vim will not work.
What I want is to make MyShell > act like interactive tclsh session. The reason why I can't just use tclsh is the loop at the last line of main.tcl: I have to have that loop and everything has to happen in that loop. I also have to do some stuff at each iteration of that loop, so can use vwait.
Here is the solution.
I have found no better solution then to overwrite the ::unknown function.
# file main.tcl
proc ::unknown { args } \
{
variable ::tcl::UnknownPending
global auto_noexec auto_noload env tcl_interactive
global myshell_evaluation
if { [info exists myshell_evaluation] && $myshell_evaluation } {
set level #0
} else {
set level 1
}
# If the command word has the form "namespace inscope ns cmd"
# then concatenate its arguments onto the end and evaluate it.
set cmd [lindex $args 0]
if {[regexp "^:*namespace\[ \t\n\]+inscope" $cmd] && [llength $cmd] == 4} {
#return -code error "You need an {*}"
set arglist [lrange $args 1 end]
set ret [catch {uplevel $level ::$cmd $arglist} result opts]
dict unset opts -errorinfo
dict incr opts -level
return -options $opts $result
}
catch {set savedErrorInfo $::errorInfo}
catch {set savedErrorCode $::errorCode}
set name $cmd
if {![info exists auto_noload]} {
#
# Make sure we're not trying to load the same proc twice.
#
if {[info exists UnknownPending($name)]} {
return -code error "self-referential recursion in \"unknown\" for command \"$name\"";
}
set UnknownPending($name) pending;
set ret [catch {
auto_load $name [uplevel $level {::namespace current}]
} msg opts]
unset UnknownPending($name);
if {$ret != 0} {
dict append opts -errorinfo "\n (autoloading \"$name\")"
return -options $opts $msg
}
if {![array size UnknownPending]} {
unset UnknownPending
}
if {$msg} {
if {[info exists savedErrorCode]} {
set ::errorCode $savedErrorCode
} else {
unset -nocomplain ::errorCode
}
if {[info exists savedErrorInfo]} {
set ::errorInfo $savedErrorInfo
} else {
unset -nocomplain ::errorInfo
}
set code [catch {uplevel $level $args} msg opts]
if {$code == 1} {
#
# Compute stack trace contribution from the [uplevel].
# Note the dependence on how Tcl_AddErrorInfo, etc.
# construct the stack trace.
#
set errorInfo [dict get $opts -errorinfo]
set errorCode [dict get $opts -errorcode]
set cinfo $args
if {[string bytelength $cinfo] > 150} {
set cinfo [string range $cinfo 0 150]
while {[string bytelength $cinfo] > 150} {
set cinfo [string range $cinfo 0 end-1]
}
append cinfo ...
}
append cinfo "\"\n (\"uplevel\" body line 1)"
append cinfo "\n invoked from within"
append cinfo "\n\"uplevel $level \$args\""
#
# Try each possible form of the stack trace
# and trim the extra contribution from the matching case
#
set expect "$msg\n while executing\n\"$cinfo"
if {$errorInfo eq $expect} {
#
# The stack has only the eval from the expanded command
# Do not generate any stack trace here.
#
dict unset opts -errorinfo
dict incr opts -level
return -options $opts $msg
}
#
# Stack trace is nested, trim off just the contribution
# from the extra "eval" of $args due to the "catch" above.
#
set expect "\n invoked from within\n\"$cinfo"
set exlen [string length $expect]
set eilen [string length $errorInfo]
set i [expr {$eilen - $exlen - 1}]
set einfo [string range $errorInfo 0 $i]
#
# For now verify that $errorInfo consists of what we are about
# to return plus what we expected to trim off.
#
if {$errorInfo ne "$einfo$expect"} {
error "Tcl bug: unexpected stack trace in \"unknown\"" {} [list CORE UNKNOWN BADTRACE $einfo $expect $errorInfo]
}
return -code error -errorcode $errorCode -errorinfo $einfo $msg
} else {
dict incr opts -level
return -options $opts $msg
}
}
}
if { ( [info exists myshell_evaluation] && $myshell_evaluation ) || (([info level] == 1) && ([info script] eq "") && [info exists tcl_interactive] && $tcl_interactive) } {
if {![info exists auto_noexec]} {
set new [auto_execok $name]
if {$new ne ""} {
set redir ""
if {[namespace which -command console] eq ""} {
set redir ">&#stdout <#stdin"
}
uplevel $level [list ::catch [concat exec $redir $new [lrange $args 1 end]] ::tcl::UnknownResult ::tcl::UnknownOptions]
dict incr ::tcl::UnknownOptions -level
return -options $::tcl::UnknownOptions $::tcl::UnknownResult
}
}
if {$name eq "!!"} {
set newcmd [history event]
} elseif {[regexp {^!(.+)$} $name -> event]} {
set newcmd [history event $event]
} elseif {[regexp {^\^([^^]*)\^([^^]*)\^?$} $name -> old new]} {
set newcmd [history event -1]
catch {regsub -all -- $old $newcmd $new newcmd}
}
if {[info exists newcmd]} {
tclLog $newcmd
history change $newcmd 0
uplevel $level [list ::catch $newcmd ::tcl::UnknownResult ::tcl::UnknownOptions]
dict incr ::tcl::UnknownOptions -level
return -options $::tcl::UnknownOptions $::tcl::UnknownResult
}
set ret [catch {set candidates [info commands $name*]} msg]
if {$name eq "::"} {
set name ""
}
if {$ret != 0} {
dict append opts -errorinfo "\n (expanding command prefix \"$name\" in unknown)"
return -options $opts $msg
}
# Filter out bogus matches when $name contained
# a glob-special char [Bug 946952]
if {$name eq ""} {
# Handle empty $name separately due to strangeness
# in [string first] (See RFE 1243354)
set cmds $candidates
} else {
set cmds [list]
foreach x $candidates {
if {[string first $name $x] == 0} {
lappend cmds $x
}
}
}
if {[llength $cmds] == 1} {
uplevel $level [list ::catch [lreplace $args 0 0 [lindex $cmds 0]] ::tcl::UnknownResult ::tcl::UnknownOptions]
dict incr ::tcl::UnknownOptions -level
return -options $::tcl::UnknownOptions $::tcl::UnknownResult
}
if {[llength $cmds]} {
return -code error "ambiguous command name \"$name\": [lsort $cmds]"
}
}
return -code error "invalid command name \"$name\""
}
proc prompt { } \
{
puts -nonewline stdout "MyShell > "
flush stdout
}
proc process { } \
{
global myshell_evaluation
set myshell_evaluation true
catch { uplevel #0 [gets stdin] } got
set myshell_evaluation false
if { $got ne "" } {
puts stderr $got
flush stderr
}
prompt
}
fileevent stdin readable process
prompt
while { true } { update; after 100 }
The idea is to modify the ::unknown function so that it handles MyShell evaluations as the ones of tclsh interactive session.
This is an ugly solution, as I am fixing the code of ::unknown function which can be different for different systems and diferent versions of tcl.
Is there any solution which circumvents these issues?
uplevel does not only evaluate a script, but it evaluates it in the stack context of the caller of the instance where it's executed. It's a pretty advanced command which should be used when you define your own execution control structures, and OFC it's TCL specific - I find myself unable to imagine how a tclsh equivalent should work.
If you just want to evaluate another script, the proper TCL command would be eval. If that other script is tclsh, why don't you just open another tclsh?
The simplest answer, I think, would be to use the approach you're using; to rewrite the unknown command. Specifically, there is a line in it that checks to make sure the current context is
Not run in a script
Interactive
At the top level
If you replace that line:
if {([info level] == 1) && ([info script] eq "") && [info exists tcl_interactive] && $tcl_interactive} {
with something that just checks the level
if ([info level] == 1} {
you should get what you want.
Vaghan, you do have the right solution. Using ::unknown is how tclsh itself provides the interactive-shell-functionality you're talking about (invoking external binaries, etc). And you've lifted that same code and included it in your MyShell.
But, if I understand your concerns about it being an "ugly solution", you'd rather not reset ::unknown ?
In which case, why not just append the additional functionality you want to the end of the pre-existing ::unknown's body (or prepend it - you choose)
If you search on the Tcl'ers wiki for "let unknown know", you'd see a simple proc which demonstrates this. It prepends new code to the existing ::unknown, so you can keep adding additional "fallback code" as you go along.
(apologies if I've misunderstood why you feel your solution is "ugly")
Instead of changing the unknown proc, I suggest that you make the changes to evaluate the expresion
if {([info level] == 1) && ([info script] eq "") && [info exists tcl_interactive] && $tcl_interactive} {
to true.
info level: call your stuff with uplevel #0 $code
info script: call info script {} to set it to an empty value
tcl_interactive. Simple: set ::tcl_interactive 1
so your code would be
proc prompt { } {
puts -nonewline stdout "MyShell > "
flush stdout
}
proc process { } {
catch { uplevel #0 [gets stdin] } got
if { $got ne "" } {
puts stderr $got
flush stderr
}
prompt
}
fileevent stdin readable process
set tcl_interactive 1
info script {}
prompt
vwait forever
I am writing an bash script that uses expect to login to a bunch of Cisco ASAs (they don't support certificate login, hence using expect), makes a change to the configuration and then logs out.
I'd like the script to move onto the next ASA if it is unable to login.
Here is the script:
#!/bin/bash
# Scriptname: set-mtu
for asa in $(cat asa-list-temp)
do
/usr/bin/expect << EndExpect
spawn ssh admin_15#$asa
expect "assword:"
send "pa$$w0rd\r"
expect ">"
send "do something\r"
expect ">"
send "exit\r"
EndExpect
done
I think I can set a timeout on expect "assword:" but I can't figure out how to get it to close the spawned ssh session and then move onto the next ASA in the for list.
First of all I would use an expect script for this and lose the bash scripting.
Then for the expect part:
You can do this by using a switch that also matches for timeout (page 12 of exploring expect). In that way you can explicitly have some action when expect timeouts.
Otherwise by setting the timeout it will just continue with the next command in line.
set timeout 60
expect {
"assword:" {
}
timeout {
exit 1 # to exit the expect part of the script
}
}
I've created something similar where I used an overall expect script to run an expect script in parallel.
multiple.exp
#!/bin/sh
# the next line restarts using tclsh \
exec expect "$0" "$#"
# multiple.exp --
#
# This file implements the running of multiple expect scripts in parallel.
# It has some settings that can be found in multiple.config
#
# Copyright (c) 2008
#
# Author: Sander van Knippenberg
#####
# Setting the variables
##
source [file dirname $argv0]/.multiple.config
# To determine how long the script runs
set timingInfo("MultipleProcesses") [clock seconds]
# ---------------------------------------------------------------------
######
# Procedure to open a file with a certain filename and retrieve the contents as a string
#
# Input: filename
# Output/Returns: content of the file
##
proc openFile {fileName} {
if {[file exists $fileName] } {
set input [open $fileName r]
} else {
puts stderr "fileToList cannot open $fileName"
exit 1
}
set contents [read $input]
close $input
return $contents
}
######
# Procedure to write text to a file with the given filename
#
# Input: string, filename
##
proc toFile {text filename} {
# Open the filename for writing
set fileId [open $filename "w"]
# Send the text to the file.
# Failure to add '-nonewline' will reslt in an extra newline at the end of the file.
puts -nonewline $fileId $text
# Close the file, ensuring the data is written out before continueing with processing
close $fileId
}
# ---------------------------------------------------------------------
# Check for the right argument
if {$argc > 0 } {
set hostfile [lindex $argv 0]
} else {
puts stderr "$argv0 --- usage: $argv0 <hosts file>"
exit 1
}
# Create the commands that can be spawned in parallel
set commands {}
# Open the file with devices
set hosts [split [openFile $hostfile] "\n"]
foreach host $hosts {
if { [string length $host] > 1 } {
lappend commands "$commandDir/$commandName $host" # Here you can enter your own command!
}
}
# Run the processes in parallel
set idlist {}
set runningcount 0
set pattern "This will never match I guess"
# Startup the first round of processes until maxSpawn is reached,
# or the commands list is empty.
while { [llength $idlist] < $maxSpawn && [llength $commands] > 0} {
set command [lindex $commands 0]
eval spawn $command
lappend idlist $spawn_id
set commands [lreplace $commands 0 0]
incr runningcount
set commandInfo($spawn_id) $command
set timingInfo($spawn_id) [clock seconds]
send_user " $commandInfo($spawn_id) - started\n"
}
# Finally start running the processes
while {$runningcount > 0} {
expect {
-i $idlist $pattern {
}
eof {
set endedID $expect_out(spawn_id)
set donepos [lsearch $idlist $endedID]
set idlist [lreplace $idlist $donepos $donepos]
incr runningcount -1
set elapsedTime [clock format [expr [clock seconds] - $timingInfo($endedID)] -format "%M:%S (MM:SS)"]
send_user " $commandInfo($endedID) - finished in: $elapsedTime\n"
# If there are more commands to execute then do it!
if {[llength $commands] > 0} {
set command [lindex $commands 0]
eval spawn $command
lappend idlist $spawn_id
set commands [lreplace $commands 0 0]
incr runningcount
set commandInfo($spawn_id) $command
set timingInfo($spawn_id) [clock seconds]
}
}
timeout {
break
}
}
}
set elapsed_time [clock format [expr [clock seconds] - $timingInfo("MultipleProcesses")] -format "%M:%S (MM:SS)"]
send_user "$argv0 $argc - finished in: $elapsedTime\n"
multiple.config
# The dir from where the commands are executed.
set commandDir "/home/username/scripts/expect/";
set commandName "somecommand.exp";
# The maximum number of simultanious spawned processes.
set maxSpawn 40;
# The maximum timeout in seconds before any of the processes should be finished in minutes
set timeout 20800;
To make the answer clear: the solution is typical, just make timeout treatment inside the curly braced expect notation. So, your Tcl/Expect part in the shell script should be:
spawn ssh user#host
expect {
"assword:" {
send "password\r"
}
timeout {
exit
}
}
expect "prompt>"
. . .
login success
. . .
Here is another example, how to expect/treat timeout to resume waiting for the result string while the spawned command is still running.