# # Copyright 2005-2013 the Boeing Company. # See the LICENSE file included in this distribution. # # # Copyright 2004-2008 University of Zagreb, Croatia. # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions # are met: # 1. Redistributions of source code must retain the above copyright # notice, this list of conditions and the following disclaimer. # 2. Redistributions in binary form must reproduce the above copyright # notice, this list of conditions and the following disclaimer in the # documentation and/or other materials provided with the distribution. # # THIS SOFTWARE IS PROVIDED BY AUTHOR AND CONTRIBUTORS ``AS IS'' AND # ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE # ARE DISCLAIMED. IN NO EVENT SHALL AUTHOR OR CONTRIBUTORS BE LIABLE # FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL # DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS # OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) # HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT # LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY # OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF # SUCH DAMAGE. # # This work was supported in part by the Croatian Ministry of Science # and Technology through the research contract #IP-2003-143. # #****f* exec.tcl/nexec # NAME # nexec -- execute program # SYNOPSIS # set result [nexec $args] # FUNCTION # Executes the sting given in args variable. The sting is not executed # if IMUNES is running in editor only mode. Execution of a string can # be local or remote. If socket can not be opened in try of a remote # execution, mode is switched to editor only mode. # INPUTS # * args -- the string that should be executed localy or remotely. # RESULT # * result -- the standard output of the executed string. #**** proc nexec { node args } { global exec_servers # debug output of every command #puts "nexec($node): $args" #if {[lsearch $args ngctl] != -1 } { # puts "XXX $args" # set fileId [open "nexec.log" a] # puts $fileId $args # close $fileId #} # safely exec the given command, printing errors to stdout if { $node == "localnode" } { ;# local execution if { [ catch {eval exec $args} e ] } { if { $e == "child process exited abnormally" } { return };# ignore puts "error executing: exec $args ($e)" } return $e } else { puts "error: nexec called with node=$node" } } proc acquireOperModeLock { mode } { global setOperMode_lock oper_mode if { ![info exists setOperMode_lock] } { set setOperMode_lock 0 } if { $oper_mode == "exec" } { # user somehow pressed start while we were still shutting down... if { $mode == "exec" } { set choice [tk_messageBox -type yesno -default no -icon warning \ -message "You have selected to start the session while the previous one is still shutting down. Are you sure you want to interrupt the shutdown? (not recommended)"] if { $choice == "no" } { set activetool select return; # return and allow previous setOperMode to finish... } # check if user pressed stop while we were starting up... } elseif { $setOperMode_lock } { ;# mode == "edit" set choice [tk_messageBox -type yesno -default no -icon warning \ -message "You are trying to stop the session while it is still starting. Are you sure you want to interrupt the startup? (not recommeded)"] if { $choice == "no" } { set activetool select return; # return and allow previous setOperMode to finish... } } } set setOperMode_lock 1 } proc releaseOperModeLock { } { global setOperMode_lock set setOperMode_lock 0 } proc checkRJ45s {} { global systype node_list g_prefs if { [lindex $systype 0] == "Linux" } { set extifcs [nexec localnode /sbin/ifconfig -a -s | tail -n +2 | awk "{ print \$1 }" | xargs] set extifcs \ [lreplace $extifcs [lsearch $extifcs lo] [lsearch $extifcs lo]] } else { set extifcs [nexec localnode ifconfig -l] set extifcs \ [lreplace $extifcs [lsearch $extifcs lo0] [lsearch $extifcs lo0]] } foreach node $node_list { if { [getNodeLocation $node] != "" } { continue } if { [nodeType $node] == "rj45" } { set i [lsearch $extifcs [getNodeName $node]] if { $i >= 0 } { continue } set msg "Error: external interface \"[getNodeName $node]\"" set msg "$msg does not exist. Press OK to continue with RJ45s" set msg "$msg disabled. NOTE: this setting can be re-enabled" set msg "$msg through Session > Options..." set choice [tk_messageBox -type okcancel -icon error -message $msg] if { $choice == "cancel" } { return -1 } setSessionOptions "" "enablerj45=0" break; } } return 0 } proc drawToolbar { mode } { global CORE_DATA_DIR global activetoolp defSelectionColor set activetoolp "" markerOptions off # # edit mode button bar # set buttons [list start link] foreach b $buttons { if { $mode == "exec"} { destroy .left.$b } else { # add buttons when in edit mode set imgf "$CORE_DATA_DIR/icons/tiny/$b.gif" set image [image create photo -file $imgf] catch { radiobutton .left.$b -indicatoron 0 \ -variable activetool -value $b -selectcolor $defSelectionColor \ -width 32 -height 32 -image $image \ -command "popupMenuChoose \"\" $b $imgf" leftToolTip $b .left pack .left.$b -side top } } } # popup toolbar buttons have submenus set buttons {routers hubs bgobjs} foreach b $buttons { if { $mode == "exec"} { destroy .left.$b } else { # create buttons for parent items set menubuttons { } if { $b == "routers" } { set menubuttons [getNodeTypeNames] } elseif { $b == "hubs" } { set menubuttons { hub lanswitch wlan rj45 tunnel } } elseif { $b == "bgobjs" } { set menubuttons { marker oval rectangle text } } set firstb [lindex $menubuttons 0] set firstfn "$CORE_DATA_DIR/icons/tiny/$firstb.gif" set image [image create photo -file $firstfn] $image read "$CORE_DATA_DIR/icons/tiny/arrow.gif" -to 27 22 # create the parent menu menubutton .left.$b -indicatoron 0 -direction right \ -width 32 -height 32 -image $image \ -padx 0 -pady 0 -relief raised \ -menu .left.${b}.menu set buttonmenu [menu .left.${b}.menu \ -activebackground $defSelectionColor \ -borderwidth 1 -tearoff 0] # create the child menutbuttons drawToolbarSubmenu $b $menubuttons # tooltips for parent and submenu items leftToolTip $b .left bind $buttonmenu <> {leftToolTipSubMenu %W} bind $buttonmenu { set newlen [expr {[string length %W] - 6}] set w [string range %W 0 $newlen] destroy ${w}.balloon } # set submenu tooltips for user-defined types to type name setLeftTooltips $b $menubuttons pack .left.$b -side top } } # # Exec mode button bar # if { "$mode" == "edit" } { .left.start configure -command "startStopButton exec" } foreach b {stop observe plot marker twonode run } { if { "$mode" != "exec" } { destroy .left.$b } else { set cmd "" set fn "$CORE_DATA_DIR/icons/tiny/$b.gif" set image [image create photo -file $fn] if { $b == "stop" } { set cmd "startStopButton edit" } elseif { $b == "observe" } { set cmd "popupObserverWidgets" } elseif { $b == "marker" } { set cmd "markerOptions on" } elseif { $b == "mobility" } { set cmd "popupMobilityDialog" } elseif { $b == "twonode" } { set cmd "popupTwoNodeDialog" } elseif { $b == "run" } { set cmd "popupRunDialog" } # add more cmds here radiobutton .left.$b -indicatoron 0 \ -variable activetool -value $b -command $cmd \ -selectcolor [.left cget -bg] \ -width 32 -height 32 -activebackground gray -image $image leftToolTip $b .left pack .left.$b -side top } } # turn off any existing tooltip balloon .left "" } proc drawToolbarSubmenu { b menubuttons } { global CORE_DATA_DIR set buttonmenu .left.${b}.menu if { ![winfo exists $buttonmenu] } { return ;# this may occur in exec mode } $buttonmenu delete 0 end foreach menubutton $menubuttons { if { $b == "routers" } { set imgf [getNodeTypeImage $menubutton tiny] } else { set imgf "$CORE_DATA_DIR/icons/tiny/${menubutton}.gif" } if { ![file exists $imgf] } { ;# pop custom filename from list puts "**warning: missing icon $imgf" continue } set img [createImageButton $imgf 0] $buttonmenu add command -image $img -columnbreak 1 \ -command "popupMenuChoose $b $menubutton $imgf" } # add an edit button to the end of the row if { $b == "routers" } { set imgf "$CORE_DATA_DIR/icons/normal/document-properties.gif" set img [createImageButton $imgf 0] $buttonmenu add command -image $img -columnbreak 1 \ -command "popupNodesConfig" } } proc setSessionStartStopMenu { mode } { if { $mode == "exec" } { catch {.menubar.session entryconfigure "Start" \ -label "Stop" -command "startStopButton edit"} } else { catch {.menubar.session entryconfigure "Stop" \ -label "Start" -command "startStopButton exec"} } } proc updateMenus { mode } { set s "normal" if { $mode == "exec" } { set s "disabled" } .menubar.tools entryconfigure "Auto rearrange all" -state $s .menubar.tools entryconfigure "Auto rearrange selected" -state $s .menubar.session entryconfigure "Node types..." -state $s .menubar.session entryconfigure "Emulation servers..." -state $s if { $s == "normal" } { set s "" } updateUndoRedoMenu $s } proc startStopButton { mode } { global activetool # # Prepare API channel for emulation. Do this before any GUI changes # so that connect failures leave the GUI in edit mode. setSystype global systype set emul_sock 0 if { $mode == "exec" } { set msg "The CORE daemon must be running and your kernel must support" set msg "$msg virtualization for the emulation to start." } elseif { $mode == "edit" } { set msg "Communication with daemon was lost." } else { puts "unsupported mode: $mode" return } set plugin [lindex [getEmulPlugin "*"] 0] set emul_sock [pluginConnect $plugin connect true] if { $emul_sock == "" || $emul_sock == -1 } { tk_messageBox -type ok -icon warning -message $msg releaseOperModeLock set activetool select return } setOperMode $mode } #****f* exec.tcl/setOperMode # NAME # setOperMode -- set operating mode # SYNOPSIS # setOperMode $mode # FUNCTION # Sets imunes operating mode to the value of the parameter mode. # The mode can be set only to edit or exec. # When changing the mode to exec all the emulation interfaces are # checked (if they are nonexistent the message is displayed, and # mode is not changed), all the required buttons are disabled # (except the simulation/Terminate button, that is enabled) and # procedure deployCfg is called. # When changing the mode to edit, all required buttons are enabled # (except for simulation/Terminate button that is disabled) and # procedure cleanupCfg is called. # INPUTS # * mode -- the new operating mode. Can be edit or exec. #**** proc setOperMode { mode } { global oper_mode activetool global undolevel redolevel global g_prefs # special handling when experiment is already running acquireOperModeLock $mode # Verify that links to external interfaces are properly configured if { $mode == "exec" && [getSessionOption enablerj45 1]==1 } { if { [checkRJ45s] < 0 } { releaseOperModeLock set activetool select return } } # start/stop menu item setSessionStartStopMenu $mode # # take care of GUI changes and bindings when switching modes # drawToolbar $mode if { $mode == "edit" } { ;# these are the default bindings .c bind node "popupConfigDialog .c" .c bind nodelabel "popupConfigDialog .c" } else { ;# double-click to open shell .c bind node "button3node .c %x %y shift" .c bind nodelabel "button3node .c %x %y shift" } set activetool select .left.select configure -state active updateMenus $mode blinkCEL "stop" # # Start/stop the emulation # ### start button is pressed if { "$mode" == "exec" } { rearrange_off set oper_mode exec resetAllNodeCoords save clearExceptions "" "" throwCEL true # Bind left mouse click to displaying the CPU usage in # a graph format bind .bottom.cpu_load <1> {manageCPUwindow %X %Y 1} monitor_loop set plugin [lindex [getEmulPlugin "*"] 0] set emul_sock [pluginConnect $plugin connect false] deployCfgAPI $emul_sock widget_loop mobility_script_loop ### stop button is pressed } else { if {$oper_mode != "edit"} { set t_start [clock seconds] shutdownSession statgraph off 0 set t_elapsed [expr {[clock seconds] - $t_start}] statline "Cleanup completed in $t_elapsed seconds." } clearLinkHighlights catch { destroy .popup } clearWlanLinks "" widgets_stop set oper_mode edit # Bind left mouse click to clearing the CPU graph bind .bottom.cpu_load <1> {manageCPUwindow %X %Y 0} manageCPUwindow %X %Y 0 } .c config -cursor left_ptr releaseOperModeLock } #****f* exec.tcl/statline # NAME # statline -- status line # SYNOPSIS # statline $line # FUNCTION # Sets the string of the status line. If the execution mode is # set to batch the line is just printed on the standard output. # INPUTS # * line -- line to be displayed #**** proc statline {line} { global execMode if {$execMode == "batch" || $execMode == "addons"} { puts $line } else { .bottom.textbox config -text "$line" animateCursor } } proc getNextMac {} { global mac_byte4 mac_byte5 set a [format %.2x $mac_byte4] set b [format %.2x $mac_byte5] incr mac_byte5 if { $mac_byte5 >= 255 } { set mac_byte5 0 incr mac_byte4 } return "00:00:00:aa:$a:$b" } #****f* exec.tcl/monitor_loop # NAME # monitor_loop -- monitor loop # SYNOPSIS # monitor_loop # FUNCTION # Calculates the usage of cpu, mbuffers and mbuf clusters. # The results are displayed in status line and updated # every two seconds. #**** proc monitor_loop {} { global oper_mode systype global server_cpuusage global exec_servers if {$oper_mode != "exec"} { .bottom.cpu_load config -text "" .bottom.mbuf config -text "" return } if { [lindex $systype 0] == "Linux" } { set cpuusage [getCPUUsage] #TODO: get the cpu usage on all the assigned server # store usage history for each server stored in an array list set assigned_servers [getAssignedRemoteServers] for {set i 0} {$i <= [llength $assigned_servers]} {incr i} { if {$i == [llength $assigned_servers]} { # localhost set ip [getMyIP] set cpuusageforserver [lindex $cpuusage 0] } else { set server [lindex $assigned_servers $i] set ip [lindex $exec_servers($server) 0] # TODO: receive CPU usage from other servers set cpuusageforserver 0 } # append the latest cpu value to the end of list and # only keep and display the last 20 values for each server set server_cpuusage($ip) [lappend server_cpuusage($ip) $cpuusageforserver] if { [llength $server_cpuusage($ip)] > 20 } { set server_cpuusage($ip) [lreplace $server_cpuusage($ip) 0 0] } } #plot the usage data if cpu windows already opened # for all servers if { [winfo exists .cpu]} { plotCPUusage } set cputxt "CPU [lindex $cpuusage 0]% (" set cpuusage [lreplace $cpuusage 0 0] for { set n 0 } { $n < [llength $cpuusage] } { incr n } { if { $n > 0 } { set cputxt "${cputxt}/" } set cputxt "${cputxt}[lindex $cpuusage $n]" } set cputxt "$cputxt)" set cpulen [expr {[string length $cputxt] - 2}] set labellen [.bottom.cpu_load cget -width] if { $cpulen < $labellen } { set cpulen $labellen } set stats [nexec localnode vmstat | tail -n 1 ] set mem_free [lindex $stats 3] set mem_free_mb [expr { $mem_free / 1024 }] .bottom.cpu_load config -text $cputxt -width $cpulen .bottom.mbuf config -text " ${mem_free_mb}M free" after 2000 { monitor_loop } return } if { $systype == "FreeBSD 4.11-RELEASE" } { set defaultname "default" set cpun 3 } else { set defaultname "." set cpun 4 } # CPU usage from `vimage -l` set vimagetext [nexec localnode vimage -l $defaultname | xargs] set vimagelist [split $vimagetext :] set cpuline [lindex $vimagelist $cpun] set cpu [lindex [split $cpuline %] 0] .bottom.cpu_load config -text "CPU $cpu%" # mbuf usage from `netstat -m` set nstout [split [nexec localnode netstat -m] ] set mbufline [split [lindex $nstout 0] /] set mbufs [lindex $mbufline 0] set nmbufs [lindex [split [lindex $mbufline 2] " "] 0] set mbufp [expr $mbufs * 100 / $nmbufs] .bottom.mbuf config -text "mbuf $mbufp%" after 2000 { monitor_loop } } #****f* exec.tcl/execSetLinkParams # NAME # execSetLinkParams -- in exec mode set link parameters # SYNOPSIS # execSetLinkParams $eid $link # FUNCTION # Sets the link parameters during execution. # All the parameters are set at the same time. # INPUTS # eid -- experiment id # link -- link id #**** proc execSetLinkParams { eid link } { set lnode1 [lindex [linkPeers $link] 0] set sock [lindex [getEmulPlugin $lnode1] 2] sendLinkMessage $sock $link modify return } # command executed when popup menu item is selected proc popupMenuChoose { parent b imgf } { global activetool activetool_prev activetool_prev_imgf activetoolp #puts "popupMenuChoose $parent -> $b ($activetoolp -> $activetool)" # deselect previous item if { $activetoolp != "" } { set img [createImageButton $activetool_prev_imgf 1] .left.$activetoolp configure -image $img -relief raised } if {$activetool_prev == "marker"} { markerOptions off } # change the active item; we need activetool_prev b/c of radio button set activetoolp $parent set activetool $b set activetool_prev $b ;# previously-selected activetool set activetool_prev_imgf $imgf ;# previously-selected button image file # hint for topogen global g_last_selected_node_type if { $activetoolp == "routers" || $activetoolp == "hubs" } { set g_last_selected_node_type [list $parent $b $imgf] } # select a new button if { $parent != "" } { set img [createImageButton $imgf 2] .left.$parent configure -image $img -relief sunken } if {$activetool == "marker"} { markerOptions on } # status message .bottom.textbox config -text "$b tool selected" } # # Boeing - create an image for use on the button toolbar # style 0 = no arrow, 1 = arrow, 2 = arrow + select color proc createImageButton { imgf style } { global CORE_DATA_DIR defSelectionColor if { [catch {set img [image create photo -file $imgf] } e] } { puts "warning: error opening button image $imgf ($e)" set img [image create photo] ;# blank button } # add an arrow to the button if { $style > 0 } { $img read "$CORE_DATA_DIR/icons/tiny/arrow.gif" -to 27 22 if { $style == 2 } { ;# highlight the button set img2 [image create photo] $img2 put [$img data -background $defSelectionColor] set img $img2 } } return $img } # Boeing: status bar graph proc statgraph { cmd n } { global statgraph_max statgraph_current tk_patchLevel execMode if { $execMode != "interactive" || ![info exists tk_patchLevel] } { return } switch -exact $cmd { on { if { $n == 0 } { return } ;# Boeing: prevent div by 0 set statgraph_max $n set statgraph_current 0 label .bottom.status -relief sunken -bd 1 -anchor w -width 2 \ -background green -justify center label .bottom.status2 -relief sunken -bd 1 -anchor w -width 12 pack .bottom.status .bottom.status2 -side left \ -before .bottom.textbox } off { set statgraph_max 0 set statgraph_current 0 destroy .bottom.status destroy .bottom.status2 } inc { # Boeing: prevent div by 0 if { $n == 0 || $statgraph_max == 0 } { return } incr statgraph_current $n set p [expr $statgraph_current.0 / $statgraph_max.0] set width [expr int($p * 15)] .bottom.status config -width $width .bottom.status config -text " ([expr int($p*100)]%)" .bottom.status2 config -width [expr 15-$width] } } } proc popupConnectMessage { dst } { global CORE_DATA_DIR execMode if { $execMode != "interactive" } { return } set wi .popupConnectMessage catch { destroy $wi } toplevel $wi wm transient $wi . wm title $wi "Connecting" set i [image create photo -file $CORE_DATA_DIR/icons/normal/lanswitch.gif] frame $wi.txt label $wi.txt.ico -image $i label $wi.txt.label1 -text "Please wait, connecting to $dst..." pack $wi.txt.ico $wi.txt.label1 -side left -padx 4 -pady 4 pack $wi.txt -side top if { [exec id -u] != 0 } { frame $wi.txt2 set longtext "(Note: remote emulation may have been enabled\n" set longtext "$longtext because you are not running as root.)" label $wi.txt2.label2 -text $longtext pack $wi.txt2.label2 -side left -padx 4 -pady 4 pack $wi.txt2 -side bottom } after 100 { catch { grab .popupConnectMessage } } update } proc popdownConnectMessage { } { catch { destroy .popupConnectMessage } } proc updateConnectMessage { dst } { set wi .popupConnectMessage catch { $wi.txt.label1 configure -text "Please wait, connecting to $dst..." } } # helper to return the list of assigned remote execution servers proc getAssignedRemoteServers {} { global node_list set servers {} foreach node $node_list { set server [getNodeLocation $node] if { $server == "" } { continue } if { [lsearch -exact $servers $server] < 0 } { lappend servers $server } } return $servers } # generate a separate window for displaying CPU proc manageCPUwindow {xpos ypos start} { global exec_servers global server_cpuusage if {$start == 1} { if { ![winfo exists .cpu]} { toplevel .cpu wm geometry .cpu 200x210+$xpos+$ypos wm resizable .cpu 0 0 wm title .cpu "CPU Usage" canvas .cpu.graph -width 200 -height 210 pack .cpu.graph } } else { if { [winfo exists .cpu]} { destroy .cpu set assigned_servers [getAssignedRemoteServers] for {set i 0} {$i <= [llength $assigned_servers]} {incr i} { if {$i == [llength $assigned_servers]} { set ip [getMyIP] } else { set server [lindex $assigned_servers $i] set ip [lindex $exec_servers($server) 0] } set server_cpuusage($ip) [lreplace $server_cpuusage($ip) 0 end] } } } } proc getMyIP { } { if { [catch {set theServer [socket -server none -myaddr \ [info hostname] 0]} ] } { return "127.0.0.1" } set myIP [lindex [fconfigure $theServer -sockname] 0] close $theServer return $myIP } # display all values stored in cpu usage history for each server proc plotCPUusage { } { global cpu_palettes global exec_servers global server_cpuusage .cpu.graph delete "all" .cpu.graph create line 0 100 200 100 -width 2 .cpu.graph create line 0 80 200 80 -width 1 .cpu.graph create line 0 60 200 60 -width 1 .cpu.graph create line 0 40 200 40 -width 1 .cpu.graph create line 0 20 200 20 -width 1 .cpu.graph create line 0 0 200 0 -width 1 .cpu.graph create line 40 0 40 100 -width 1 .cpu.graph create line 80 0 80 100 -width 1 .cpu.graph create line 120 0 120 100 -width 1 .cpu.graph create line 160 0 160 100 -width 1 .cpu.graph create line 200 0 200 100 -width 1 # for each server create a plot of cpu usage set assigned_servers [getAssignedRemoteServers] for {set i 0} {$i <= [llength $assigned_servers]} {incr i} { if {$i == [llength $assigned_servers]} { set ip [getMyIP] } else { set server [lindex $assigned_servers $i] set ip [lindex $exec_servers($server) 0] } #need to add multiple cpuusgaehistory (array) for { set n 1 } { $n < [llength $server_cpuusage($ip)] } { incr n } { set prevn [expr {$n - 1}] set x1 [expr {$prevn * 10}] set y1 [expr {100 - [lindex $server_cpuusage($ip) $prevn]}] set x2 [expr {$n * 10}] set y2 [expr {100 - [lindex $server_cpuusage($ip) $n]}] if {$i < [llength $cpu_palettes]} { .cpu.graph create line $x1 $y1 $x2 $y2 -fill [lindex $cpu_palettes $i] } else { .cpu.graph create line $x1 $y1 $x2 $y2 -fill [lindex $cpu_palettes end] } #debug #puts " cpu $x1 $y1 $x2 $y2" } #for each server create a legend (limited to 8) set legendtext $ip append legendtext " " [lindex $server_cpuusage($ip) end] "%" set legendy [expr {($i * 10) + 120}] set legendx 10 if {$i < [llength $cpu_palettes]} { .cpu.graph create rectangle $legendx $legendy \ [expr {$legendx + 8}] [expr {$legendy + 8}] \ -fill [lindex $cpu_palettes $i] .cpu.graph create text [expr {$legendx + 15}] [expr {$legendy + 4}]\ -text $legendtext -fill [lindex $cpu_palettes $i] \ -anchor w -justify left } else { .cpu.graph create rectangle $legendx $legendy \ [expr {$legendx + 8}] [expr {$legendy + 8}] \ -fill [lindex $cpu_palettes end] .cpu.graph create text [expr {$legendx + 15}] [expr {$legendy + 4}]\ -text $legendtext -fill [lindex $cpu_palettes end] \ -anchor w -justify left } } }