This is a procedure of one of the items of the status bar on i3 window manager on linux. Is run every second. Basically deals with frequency governors. If temperature reaches a certain number then switch to powersave mode, or if a certain application is running, e.g. steam, or the laptop is running on batteries. If temperature reaches a lower point then it will switch to performance, etc.
The procedure runs very well so far, no issues. However the code has so many nested if-else statements, that it hard to maintain and everytime I add something the code becomes more, well.... nested.
proc cpu_freq {} {
set app steam
set cpu_power [exec sudo cpupower frequency-info | sed -ne /speed/p]
set cpu_temp [exec sensors | grep Core | sed -n {2p} | awk {{print $3}} | cut -c2-3]
set battery [exec acpi]
if {[string match *performance* $cpu_power]} {set cpu_freq HIGH; set color "$::green"}
if {[string match *powersave* $cpu_power]} {set cpu_freq LOW; set color "$::red"}
if {![file isfile $::i3dir/powersave.freq] && ![file isfile $::i3dir/performance.freq]} {
set switch AUTO
}
# ON BATTERY
if {[string match *Discharging* $battery]} {
# WHEN IN PERFORMANCE MODE
if {[string match *performance* $cpu_power]} {
if {![file isfile $::i3dir/performance.freq]} {
# AND NOT IN MANUAL
# SWITCH TO POWERSAVE
exec sudo cpupower frequency-set -g powersave
set cpu_freq LOW
set switch AUTO
set color "$::red"
set ::on_battery true
} else {
# SWITCH TO MANUAL PERFORMANCE MODE
if {[file isfile $::i3dir/performance.freq]} {
exec sudo cpupower frequency-set -g performance
set cpu_freq HIGH
set switch MAN
set color "$::green"
set ::on_battery true
} else {
if {[file isfile $::i3dir/powersave.freq]} {
# SWITCH TO MANUAL POWERSAVE MODE
exec sudo cpupower frequency-set -g powersave
set cpu_freq LOW
set switch MAN
set color "$::red"
set ::on_battery true
}
}
}
} else {
# WHEN IN POWERSAVE MODE (AUTO)
# SWITCH TO MANUAL POWERSAVE
if {[string match *powersave* $cpu_power]} {
if {[file isfile $::i3dir/powersave.freq]} {
exec sudo cpupower frequency-set -g powersave
set cpu_freq LOW
set switch MAN
set color "$::red"
set ::on_battery true
} else {
# SWITCH TO MANUAL PERFORMANCE
if {[file isfile $::i3dir/performance.freq]} {
exec sudo cpupower frequency-set -g performance
set cpu_freq HIGH
set switch MAN
set color "$::green"
set ::on_battery true
}
}
}
}
# ON MAINS
} else {
# WHEN IN POWERSAVE MODE
if {[string match *powersave* $cpu_power]} {
# RUNNING APP OR MANUAL SWITCH
if {[file isfile $::i3dir/powersave.freq]} {
set cpu_freq LOW
set switch MAN
} else {
if {[isRunning $app]} {
set cpu_freq LOW
set switch AUTO
# DO NOTHING, KEEP RUNNING IN POWERSAVE MODE
} else {
# SWITCH TO PERFORMANCE AFTER RUNNING ON BATTERIES
if {$::on_battery==true} {
exec sudo cpupower frequency-set -g performance
set cpu_freq HIGH
set switch AUTO
set color "$::green"
set ::on_battery false
# SWITCH TO PERFORMANCE WHEN REACHING LOWER TEMPS
} elseif {$cpu_temp <= 55} {
exec sudo cpupower frequency-set -g performance
set cpu_freq HIGH
set switch AUTO
set color "$::green"
}
}
}
# WHEN IN PERFORMANCE MODE
} else {
# MANUAL SWITCH
if {[file isfile $::i3dir/performance.freq]} {
set switch MAN
set cpu_freq HIGH
# DO NOTHING, KEEP RUNNING IN PERFORMANCE MODE
} else {
# HOT TEMPERATURE OR RUNNING APP
# SWITCH TO POWERSAVE
if {$cpu_temp >= 75 || [isRunning $app] } {
exec sudo cpupower frequency-set -g powersave
set cpu_freq LOW
set switch AUTO
set color "$::red"
} else {
set cpu_freq HIGH
set switch AUTO
}
}
}
}
set stdout {{"name":"cpu_freq","full_text":"$switch:$cpu_freq","color":"$color"}}
set stdout [subst -nocommands $stdout]
puts -nonewline $stdout
}
Break it up into a set of functions.
Tcl has a switch statement which can sometimes help. It also has elseif to assist in reducing nesting. But in the code shown, break it into functions with sensible names and you can reduce it to one function that deals with the logic and a collection that deal with what happens in a certain case.
When I see something like this I immediately think about finite state machines/state transition diagrams. You have a starting state and you then switch to other states based on the results of the procs that you call in the if statements, at some point you reach an end state from which no further transitions are possible.
So I'd look at restructuring to something like the following example:
# The value to process
set value "This is a big red ball"
# The starting state
set state 1
# The state transtions and the functions to implement them
set states [dict create "1,3" "IsRed" "1,2" "IsBlue" "2,4" "IsBig" "2,5" "IsSmall" "3,4" "IsBig" "3,5" "IsSmall"]
# Procs that implement the state transitions
proc IsRed {next} {
global value state
if {[string first "red" $value] != -1} {
puts "red"
set state $next
return true
}
return false
}
proc IsBlue {next} {
global value state
if {[string first "blue" $value] != -1} {
puts "blue"
set state $next
return true
}
return false
}
proc IsSmall {next} {
global value state
if {[string first "small" $value] != -1} {
puts "small"
set state $next
return true
}
return false
}
proc IsBig {next} {
global value state
if {[string first "big" $value] != -1} {
puts "big"
set state $next
return true
}
return false
}
# Proc to run the state machine until the state stops changing
proc runMachine { states } {
global state
set startState -1
while { $state != $startState } {
set startState $state
foreach key [dict keys $states "$state,*"] {
set next [lindex [split $key ","] 1]
set res [[dict get $states $key] $next]
# If the state changes then no need to do any more processing
if { $res == true } {
break
}
}
}
}
runMachine $states
This is one possible approach and it's much simpler than what you need to do but shows the basic idea. The dictionary shows the allowed state transitions and the proc to run in order to test if the transition is allowed. I've put my processing code (the puts statement) in this function but it would be simple to have another function do the processing, either called directly or held as another value in the dictionary and called from the runMachine proc.
set states [dict create 21,3" [list "IsRed" "RedAction"]]
This approach lets you seperate all the actions and transitions out and draw a state transition diagram that clearly shows what's going on.
A quick google for TCL Finite State Machine shows lots of other ways to implement this idea.
Breaking the code up into separate functions as suggested by patthoyts is a good solution but might possibly be somewhat slower (it's unlikely that you'll notice, however). Another solution to make the code easier to work with is to dynamically create cpu_freq during startup.
To do this, write a script that is as verbose and with as much documentation as you want, which produces the succinct and efficient body that you want for cpu_freq. When you need to extend it, you just add more sections to the script. Call proc with the produced body as third argument, and it will be compiled the first time it is called.
Related
The following code attempts to react to one Supply and then, based on the content of some message, change its mind and react to messages from a different Supply. It's an attempt to provide similar behavior to Supply.migrate but with a bit more control.
my $c1 = Supplier.new;
my $c2 = Supplier.new;
my $s = supply {
my $currently-listening-to = $c1.Supply;
my $other-var = 'foo';
whenever $currently-listening-to {
say "got: $_";
if .starts-with('3') {
say "listening to something new";
$currently-listening-to = $c2.Supply;
$other-var = 'bar';
say $other-var;
}
}
}
$s.tap;
for ^7 {
$c1.emit: "$_ from \$c1";
$c2.emit: "$_ from \$c2";
}
sleep 10;
If I understand the semantics of supply blocks correctly (highly doubtful!), this block should have exclusive and mutable access to any variables declared inside the supply block. Thus, I expected this to get the first 4 values from $c1 and then switch to $c2. However, it doesn't. Here's the output:
ot: 0 from $c1
got: 1 from $c1
got: 2 from $c1
got: 3 from $c1
listening to something new
bar
got: 4 from $c1
got: 5 from $c1
got: 6 from $c1
As that output shows, changing $other-var worked just as I expected it to, but the attempt to change $currently-listening-to failed (silently).
Is this behavior correct? If so, what am I missing about the semantics of supply blocks/other constructs that explains this behavior? I got the same results with react blocks and when using a Channel instead of a Supply, so the behavior is consistent across several multiple concurrency constructs.
(In the interest of avoiding an X-Y problem, the use case that triggered this question was an attempt implement Erlang-style error handling. To do so, I wanted to have a supervising supply block that listened to its children and could kill/re-launch any children that got into a bad state. But that means listening to the new children – which led directly to the issue described above.)
I tend to consider whenever as the reactive equivalent of for. (It even supports the LAST loop phaser for doing something when the tapped Supply is done, as well as supporting next, last, and redo like an ordinary for loop!) Consider this:
my $x = (1,2,3);
for $x<> {
.say;
$x = (4,5,6);
}
The output is:
1
2
3
Because at the setup stage of a for loop, we obtain an iterator, and then work through that, not reading $x again on each iteration. It's the same with whenever: it taps the Supply and then the body is invoked per emit event.
Thus another whenever is needed to achieve a tap of the next Supply, while simultaneously closing the tap on the current one. When there are just two Supplys under consideration, the easy way to write it is like this:
my $c1 = Supplier.new;
my $c2 = Supplier.new;
my $s = supply {
whenever $c1 {
say "got: $_";
if .starts-with('3') {
say "listening to something new";
# Tap the next Supply...
whenever $c2 {
say "got: $_";
}
# ...and close the tap on the current one.
last;
}
}
}
$s.tap;
for ^7 {
$c1.emit: "$_ from \$c1";
$c2.emit: "$_ from \$c2";
}
Which will produce:
got: 0 from $c1
got: 1 from $c1
got: 2 from $c1
got: 3 from $c1
listening to something new
got: 3 from $c2
got: 4 from $c2
got: 5 from $c2
got: 6 from $c2
(Note that I removed the sleep 10 because there's no need for it; we aren't introducing any concurrency in this example, so everything runs synchronously.)
Clearly, if there were a dozen Supplys to move between then this approach won't scale so well. So how does migrate work? The key missing piece is that we can obtain the Tap handle when working with whenever, and thus we are able to close it from outside of the body of that whenever. This is exactly how migrate works (copied from the standard library, with comments added):
method migrate(Supply:D:) {
supply {
# The Tap of the Supply we are currently emitting values from
my $current;
# Tap the Supply of Supply that we'll migrate between
whenever self -> \inner {
# Make sure we produce a sensible error
X::Supply::Migrate::Needs.new.throw
unless inner ~~ Supply;
# Close the tap on whatever we are currently tapping
$current.close if $current;
# Tap the new thing and store the Tap handle
$current = do whenever inner -> \value {
emit(value);
}
}
}
}
In short: you don't change the target of the whenever, but rather start a new whenever and terminate the previous one.
I thought my tests were complete , in the lreport I can see all lines covered... but I get
97.44% Statements 38/39 100% Branches 16/16 66.67% Functions 2/3 100% Lines 38/38
I re-tested adding non useful else branches and console logs ... every case is tested...
what's wrong with it ?
As jesse found where was the issue ... I added the istanbul ignore ...
[types.START] (state) {
state.started = true
state.paused = false
state.stopped = false
/* istanbul ignore next */
state.interval = setInterval(() => tick(state), 1000)
if (state.isWorking && state.soundEnabled) {
Vue.noise.start()
}
},
and now the coverage is 100%...
Is there any way to list out all user defined proc calls in a Tcl file. ?
I have a TCL file, I want to list out all procs that has been called by that file.
Regards
keerthan
To get a message when a specific command is called, we can use an enter execution trace:
trace add execution $theCommand enter {apply {{call op} {
puts "Called [lindex $call 0]"
}}
To select which command to attach that trace to, we can use various approaches. The easiest is if you can identify a namespace whose procedures you want to attach to:
foreach p [info procs ::theNamespace::*] {
trace add execution $p enter {apply {{call op} {
puts "Called [lindex $call 0]"
}}
}
You can also attach it to every procedure created when a particular file is sourced. This is a bit more complex:
# Need to keep this script fragment so we can remove it again after the source
set tracer {apply {{proccall args} {
set cmd [uplevel 1 [list namespace origin [lindex $proccall 1]]]
trace add execution $cmd enter {apply {{call args} {
puts "Called [lindex $call 0]"
}}
}}
trace add execution ::proc leave $tracer
source /the/script.tcl
trace remove execution ::proc leave $tracer
You can get pretty complicated this way (and be aware that it also can affect procedures created by the loading of packages by the script; you can stop that, but it's a lot more work). Just attaching to all procedures that are currently in a namespace is simpler.
My expect script connects to several remote servers successfully and echos commands, but I can't manage to have it echo an item from a list.
For example, when sshing to server1 I'd like to output to terminal fruit:apple
But apple is saved in expect while the send sends it to a local terminal where the list is not defined. Is it possible to send expect variable to bash?
In particular the lines relevant to this from the code:
set counter 0
set types {apple orange}
set var $types($counter)
send -- "echo 'fruit:$var'\r"
set $counter [expr $counter+1]
Full code:
#!/usr/bin/expect -f
# ./sshlogin.exp uptime
#declare hosts array"
set hosts {server1 server2}
set types {apple orange}
# setting credentials
set user jack
set password welcome
set counter 0
foreach vm $hosts {
set var $types($counter)
set timeout -1
# now ssh
spawn ssh $user#$vm -o StrictHostKeyChecking=no
match_max 100000 # Look for passwod prompt
expect "*?assword:*"
# Send password aka $password
send -- "$password\r"
# send blank line (\r) to make sure we get back to gui
expect "$ "
send -- "echo 'fruit:$var'\r"
expect "$ "
send -- "exit\r"
set $counter [expr $counter+1]
expect eof }
This is wrong
set counter 0
set types {apple orange}
set var $types($counter)
Tcl has lists which are numerically indexed arrays, and arrays which are associative arrays (hashes).
You access elements of a list with, typically, the lindex command.
You access elements of an array with the $arrname($key) syntax
To address the immediate problem with those 3 lines: you want
set var [lindex $types $counter]
Your answer is the perfect way to iterate over 2 lists, pulling out elements with the same numeric index.
Running through the Tcl tutorial would be beneficial.
Added the second list to the foreach loop, and since both are of the same length it works great.
foreach looks like this now:
foreach vm $hosts fruit $types {....
This link contains an example:
http://wiki.tcl.tk/1018
Given the following in a CGI script with Perl and taint mode I have not been able to get past the following.
tail /etc/httpd/logs/error_log
/usr/local/share/perl5/Net/DNS/Dig.pm line 906 (#1)
(F) You tried to do something that the tainting mechanism didn't like.
The tainting mechanism is turned on when you're running setuid or
setgid, or when you specify -T to turn it on explicitly. The
tainting mechanism labels all data that's derived directly or indirectly
from the user, who is considered to be unworthy of your trust. If any
such data is used in a "dangerous" operation, you get this error. See
perlsec for more information.
[Mon Jan 6 16:24:21 2014] dig.cgi: Insecure dependency in eval while running with -T switch at /usr/local/share/perl5/Net/DNS/Dig.pm line 906.
Code:
#!/usr/bin/perl -wT
use warnings;
use strict;
use IO::Socket::INET;
use Net::DNS::Dig;
use CGI;
$ENV{"PATH"} = ""; # Latest attempted fix
my $q = CGI->new;
my $domain = $q->param('domain');
if ( $domain =~ /(^\w+)\.(\w+\.?\w+\.?\w+)$/ ) {
$domain = "$1\.$2";
}
else {
warn("TAINTED DATA SENT BY $ENV{'REMOTE_ADDR'}: $domain: $!");
$domain = ""; # successful match did not occur
}
my $dig = new Net::DNS::Dig(
Timeout => 15, # default
Class => 'IN', # default
PeerAddr => $domain,
PeerPort => 53, # default
Proto => 'UDP', # default
Recursion => 1, # default
);
my #result = $dig->for( $domain, 'NS' )->to_text->rdata();
#result = sort #result;
print #result;
I normally use Data::Validate::Domain to do checking for a “valid” domain name, but could not deploy it in a way in which the tainted variable error would not occur.
I read that in order to untaint a variable you have to pass it through a regex with capture groups and then join the capture groups to sanitize it. So I deployed $domain =~ /(^\w+)\.(\w+\.?\w+\.?\w+)$/. As shown here it is not the best regex for the purpose of untainting a domain name and covering all possible domains but it meets my needs. Unfortunately my script is still producing tainted failures and I can not figure out how.
Regexp-Common does not provide a domain regex and modules don’t seem to work with untainting variable so I am at a loss now.
How to get this thing to pass taint checking?
$domain is not tainted
I verified that your $domain is not tainted. This is the only variable you use that could be tainted, in my opinion.
perl -T <(cat <<'EOF'
use Scalar::Util qw(tainted);
sub p_t($) {
if (tainted $_[0]) {
print "Tainted\n";
} else {
print "Not tainted\n";
}
}
my $domain = shift;
p_t($domain);
if ($domain =~ /(^\w+)\.(\w+\.?\w+\.?\w+)$/) {
$domain = "$1\.$2";
} else {
warn("$domain\n");
$domain = "";
}
p_t($domain);
EOF
) abc.def
It prints
Tainted
Not tainted
What Net::DNS::Dig does
See Net::DNS::Dig line 906. It is the beginning of to_text method.
sub to_text {
my $self = shift;
my $d = Data::Dumper->new([$self],['tobj']);
$d->Purity(1)->Deepcopy(1)->Indent(1);
my $tobj;
eval $d->Dump; # line 906
…
From new definition I know that $self is just hashref containing values from new parameters and several other filled in the constructor. The evaled code produced by $d->Dump is setting $tobj to a deep copy of $self (Deepcopy(1)), with correctly set self-references (Purity(1)) and basic pretty-printing (Indent(1)).
Where is the problem, how to debug
From what I found out about &Net::DNS::Dig::to_text, it is clear that the problem is at least one tainted item inside $self. So you have a straightforward way to debug your problem further: after constructing the $dig object in your script, check which of its items is tainted. You can dump the whole structure to stdout using print Data::Dumper::Dump($dig);, which is roughly the same as the evaled code, and check suspicious items using &Scalar::Util::tainted.
I have no idea how far this is from making Net::DNS::Dig work in taint mode. I do not use it, I was just curious and wanted to find out, where the problem is. As you managed to solve your problem otherwise, I leave it at this stage, allowing others to continue debugging the issue.
As resolution to this question if anyone comes across it in the future it was indeed the module I was using which caused the taint checks to fail. Teaching me an important lesson on trusting modules in a CGI environment. I switched to Net::DNS as I figured it would not encounter this issue and sure enough it does not. My code is provided below for reference in case anyone wants to accomplish the same thing I set out to do which is: locate the nameservers defined for a domain within its own zone file.
#!/usr/bin/perl -wT
use warnings;
use strict;
use IO::Socket::INET;
use Net::DNS;
use CGI;
$ENV{"PATH"} = ""; // Latest attempted fix
my $q = CGI->new;
my $domain = $q->param('domain');
my #result;
if ( $domain =~ /(^\w+)\.(\w+\.?\w+\.?\w+)$/ ) {
$domain = "$1\.$2";
}
else {
warn("TAINTED DATA SENT BY $ENV{'REMOTE_ADDR'}: $domain: $!");
$domain = ""; # successful match did not occur
}
my $ip = inet_ntoa(inet_aton($domain));
my $res = Net::DNS::Resolver->new(
nameservers => [($ip)],
);
my $query = $res->query($domain, "NS");
if ($query) {
foreach my $rr (grep { $_->type eq 'NS' } $query->answer) {
push(#result, $rr->nsdname);
}
}
else {
warn "query failed: ", $res->errorstring, "\n";
}
#result = sort #result;
print #result;
Thanks for the comments assisting me in this matter, and SO for teaching more then any other resource I have come across.