# Copyright (C) 2019 bw1 # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU 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 General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program. If not, see . use strict; use vars qw($VERSION %IRSSI); use Irssi; use Irssi::TextUI; use Time::Piece; use Time::Seconds; use Getopt::Long qw/GetOptionsFromString/; use IPC::Open3; use YAML qw/Dump DumpFile LoadFile/; use Time::HiRes qw/sleep alarm/; $VERSION = '0.1'; %IRSSI = ( authors => 'bw1', contact => '[email protected]', name => 'boinc', description => 'interface to boinc', license => 'gpl', url => 'https://scripts.irssi.org/', modules => 'Time::Piece Time::Seconds Getopt::Long IPC::Open3 YAML Time::HiRes', changed => '2019-05-29', ); my $help = <<'END'; %9NAME%9 boinc.pl - interface to boinc %9SYNOPSIS%9 /boinc {-all|-tasks|-credit|-old|-info|-update|-help|-h|-list} /boinc <-host hostname> [-password password] [{-enable|-disable}] %9DESCRIPTION%9 interface to the boinc-client (https://boinc.berkeley.edu/) add the statusbar item /STATUSBAR window ADD boinc_credit %9OPTIONS%9 -all view all at once -tasks view the actual tasks -credit view the credits -old view old tasks -info print all internal data (debug) -update update the statusbar item -help|-h show a help message -list list the configured hosts -host add or modify a host entry %9SETTINGS%9 /set boinc_update_cycle 15 update time in minutes of the statusbar item. /set boinc_command boinccmd command line interface of the BOINC client. END my ($a_host, $a_password, $a_disable, $a_enable); my %options = ( 'all' => \&cmd_all, 'tasks' => \&cmd_tasks, 'credit' => \&cmd_credit, 'old' => \&cmd_old, 'info' => \&cmd_info, 'update' => \&cmd_update, 'help' => \&cmd_help, 'h' => \&cmd_help, 'host=s' => \$a_host, 'password=s' => \$a_password, 'disable' => sub {$a_disable =1}, 'enable' => sub {$a_disable =0}, 'list' => \&cmd_clist, ); my $boinc_cmd; #='boinccmd'; my $boinc_cmd_host='--host'; my $boinc_cmd_passwd='--passwd'; my $boinc_cmd_state='--get_state'; my $boinc_cmd_old_task='--get_old_tasks'; my %section= ( '======== Tasks ========'=> 'tasks', '======== Projects ========'=> 'proj', '======== Applications ========'=> 'app', '======== Application versions ========'=> 'app_ver', '======== Workunits ========'=> 'units', '======== Time stats ========'=> 'time', '======== end ========'=> 'end', # help the last round ); my $arry_start ='-----------'; my ($args, $server, $witem); my %config; my %info; my ($pid, %readex, $instr, $errstr); my $total_credit; my $expavg_credit; my $run_tasks; my ($time_tag, $time_cycle); sub mytime { my ($tstr) = @_; #my $tstr='Sun May 5 18:40:48 2019'; my $t= Time::Piece->strptime($tstr,'%a %b %d %H:%M:%S %Y'); return $t->strftime('%Y-%m-%d %H:%M'); } sub mydifftime { my ( $ts2) = @_; #my $tstr='Sun May 5 18:40:48 2019'; my $t1= localtime; my $t2= Time::Piece->strptime($ts2,'%a %b %d %H:%M:%S %Y'); my $td= $t2-$t1; return $td->hours; } sub read_exec { my ($cmd, $host, $rfunc) = @_; my ($in, $out, $err); use Symbol 'gensym'; $err = gensym; $pid = open3($in, $out, $err, $cmd); $readex{$pid}->{pid}=$pid; $readex{$pid}->{cmd}=$cmd; $readex{$pid}->{in}=$in; $readex{$pid}->{out}=$out; $readex{$pid}->{err}=$err; $readex{$pid}->{host}=$host; $readex{$pid}->{rfunc}=$rfunc; Irssi::pidwait_add($pid); } sub sig_read_exec { my ($pid, $status) = @_; if (defined $readex{$pid} ) { my $out =$readex{$pid}->{out}; my $err =$readex{$pid}->{err}; my $host =$readex{$pid}->{host}; my $rfunc =$readex{$pid}->{rfunc}; delete $readex{$pid}; my $old = select $out; local $/; $instr = <$out>; select $old; my $old = select $err; local $/; $errstr = <$err>; $errstr =~ s/[\n\r]//g; select $old; &$rfunc($host) if (defined $rfunc); if ( scalar(keys(%readex)) == 1 && exists $readex{job}) { foreach my $j ( @{$readex{job}} ) { if ( ref( $j) eq 'CODE' ) { &$j(); } else { eval( $j ); } } delete $readex{job}; } Irssi::signal_stop(); } } sub read_host_old_task { my ($host) = @_; if ( $errstr ne '') { $info{$host}->{error}=$errstr; } my @lines = split /\n/,$instr; push @lines, 'task last:'; my $count=0; my $tl=[]; my $task={}; foreach my $l (@lines) { if ($l =~ m/^task (.*):$/) { if ( $count >0 ) { push @{$tl}, $task; $task={}; } $task->{taskname}=$1; } else { if ($l =~ m/^\s+(.*?):\s*(.*)$/ ) { $task->{$1}=$2; } } $count++; } $info{$host}->{old_tasks}=$tl; } sub read_host_state { my ($host) = @_; my $h; if (!exists $info{$host}) { my $h={}; $info{$host}=$h; } else { $h=$info{$host}; } my $array=0; if ( $errstr ne '') { $info{$host}->{error}=$errstr; } my @lines = split /\n/,$instr; push @lines, '======== end ========'; my $sec=''; my $sec_c=0; my $sec_r=''; my $arr_c=0; my $arr_e=0; my $arr_r=''; my $par_r={}; foreach my $line ( @lines) { # section if (exists $section{$line}) { if ($sec_c >0) { if ( $arr_c ==0) { $h->{$sec}=$par_r; $par_r={}; } else { $h->{$sec}=$arr_r; } } else { } $sec=$section{$line}; $sec_c++; $sec_r=''; $arr_e=1; } # array if ($line =~ m/$arry_start/ || $arr_e != 0 ) { if ($arr_c >0) { push @{$arr_r}, $par_r; $par_r={}; } else { $arr_r=[]; } $arr_c++; if ($arr_e != 0) { $arr_e=0; $arr_c=0; } } # parameter if ($line =~ m/^\s+(.*?):\s*(.*)$/ ) { $par_r->{$1}=$2; } } } sub read_hosts { my ( $jobs ) = @_; if (!exists $readex{job}) { $readex{job}=$jobs; foreach my $host (keys %config) { if ($config{$host}->{disable} != 1) { $info{$host}={}; my $cmd="$boinc_cmd $boinc_cmd_host $host"; if ( defined $config{$host}->{password} ) { $cmd = "$cmd $boinc_cmd_passwd $config{$host}->{password}"; } my $cmd1="$cmd $boinc_cmd_state"; read_exec($cmd1, $host, \&read_host_state ); my $cmd2="$cmd $boinc_cmd_old_task"; read_exec($cmd2, $host, \&read_host_old_task ); } else { delete $info{$host}; } } } } sub calc_credit { $total_credit=0; $expavg_credit; $run_tasks=0; foreach my $host (sort keys %info) { my $h=$info{$host}; my $utc=0; my $uec=0; foreach my $prj (@{$h->{proj}}) { $utc += $prj->{user_total_credit}; $uec += $prj->{user_expavg_credit}; } if ($utc > $total_credit) { $total_credit=$utc; $expavg_credit=$uec; } foreach my $t (@{$h->{tasks}}) { if ($t->{active_task_state} eq 'EXECUTING') { $run_tasks++; } } } } sub print_tasks { foreach my $host (sort keys %info) { Irssi::printformat(MSGLEVEL_CLIENTCRAP,'myhl',$host); foreach my $t ( @{$info{$host}->{tasks}} ) { my $s=''; $s .= sprintf " %s ", substr($t->{name},0,3); $s .= sprintf "%5.1f ", $t->{'fraction done'}*100; my $st='W'; $st='%gR%N' if ($t->{active_task_state} eq 'EXECUTING'); $st='%bD%N' if ($t->{'ready to report'} eq 'yes'); $s .= sprintf "%s ", $st; $s .= mytime($t->{received})." "; $s .= mytime($t->{'report deadline'})." "; $s .= sprintf "%8.1f", mydifftime($t->{'report deadline'}); Irssi::print($s, MSGLEVEL_CLIENTCRAP); } } Irssi::print('', MSGLEVEL_CLIENTCRAP); } sub print_old_tasks { foreach my $host (sort keys %info) { Irssi::printformat(MSGLEVEL_CLIENTCRAP,'myhl',$host); foreach my $t ( @{$info{$host}->{old_tasks}} ) { my $s=''; $s .= sprintf " %-20s", $t->{taskname}; $s .= sprintf " %3d", $t->{'exit status'}; $s .= sprintf " %8.2fh", $t->{'elapsed time'} /60/60; Irssi::print($s, MSGLEVEL_CLIENTCRAP); } } Irssi::print('', MSGLEVEL_CLIENTCRAP); } sub print_credit { my $total_credit=0; my $expavg_credit; my $he= sprintf "%-20s %10s %10s", 'host', 'avg', 'total'; Irssi::printformat(MSGLEVEL_CLIENTCRAP,'myhl',$he); foreach my $host (sort keys %info) { my $h=$info{$host}; my $hec=0; my $htc=0; my $utc=0; my $uec=0; foreach my $prj (@{$h->{proj}}) { $hec += $prj->{host_expavg_credit}; $htc += $prj->{host_total_credit}; $utc += $prj->{user_total_credit}; $uec += $prj->{user_expavg_credit}; } my $sh= sprintf ' %-19s',$host; my $sa .= sprintf "%10.0f", $hec; my $st .= sprintf "%10.0f", $htc; Irssi::printformat(MSGLEVEL_CLIENTCRAP,'myhl', $sh, $sa, $st); if ($utc > $total_credit) { $total_credit=$utc; $expavg_credit=$uec; } } my $str= sprintf "%-20s %10.0f %10.0f",'sum', $expavg_credit, $total_credit; Irssi::printformat(MSGLEVEL_CLIENTCRAP,'myhl',$str); Irssi::print('', MSGLEVEL_CLIENTCRAP); } sub print_error { foreach my $h (keys %info) { if ( exists $info{$h}->{error} ) { Irssi::print "Error: $info{$h}->{error}", MSGLEVEL_CLIENTCRAP; } } } sub sb_boinc_credit { my ($sb_item, $get_size_only) = @_; my $sb = sprintf "run:%d avg:%d sum:%d", $run_tasks, $expavg_credit, $total_credit; $sb_item->default_handler($get_size_only, "{sb $sb}", '', 0); } sub cmd { ($args, $server, $witem)=@_; my ($ret, $arg) = GetOptionsFromString($args, %options); if (defined $a_host) { $config{$a_host}->{host}=$a_host; $config{$a_host}->{password}=$a_password if (defined $a_password); $config{$a_host}->{disable}=$a_disable if (defined $a_disable); $a_host=undef; $a_password=undef; $a_disable=undef; $a_enable=undef; } } sub cmd_clist { my $s; $s =sprintf '%-20s %-20s %-2s','host','password','disable'; Irssi::print $s, MSGLEVEL_CLIENTCRAP; foreach my $h (sort keys %config) { $s =sprintf '%-20s %-20s %2d', $config{$h}->{host}, $config{$h}->{password}, $config{$h}->{disable}; Irssi::print $s, MSGLEVEL_CLIENTCRAP; } } sub cmd_all { read_hosts([ \&print_tasks, \&print_credit, \&print_old_tasks, \&print_error, ]); } sub cmd_tasks { read_hosts([ \&print_tasks, \&print_error, ]); } sub cmd_credit { read_hosts([ \&print_credit, \&print_error, ]); } sub cmd_old { read_hosts([ \&print_old_tasks, \&print_error, ]); } sub cmd_info { Irssi::print "%info", MSGLEVEL_CLIENTCRAP; Irssi::print Dump(\%info), MSGLEVEL_CLIENTCRAP; } sub cmd_update { read_hosts(); $readex{job} = [ \&calc_credit, 'Irssi::statusbar_items_redraw("boinc_credit");', ]; } sub cmd_help { Irssi::print $help, MSGLEVEL_CLIENTCRAP; } sub write_config { my $fn=Irssi::get_irssi_dir().'/boinc.yaml'; DumpFile( $fn, \%config); } sub read_config { my $fn=Irssi::get_irssi_dir().'/boinc.yaml'; if (-e $fn) { %config = %{ LoadFile($fn) }; } } sub sig_setup_changed { $boinc_cmd= Irssi::settings_get_str('boinc_command'); my $new_time = Irssi::settings_get_int('boinc_update_cycle'); if ( $time_cycle != $new_time ) { if ( defined $time_tag) { Irssi::timeout_remove($time_tag); $time_tag= undef; $time_cycle= 0; } if ($new_time !=0 ) { $time_tag= Irssi::timeout_add($new_time*60*1000, \&cmd_update, ''); cmd_update(); $time_cycle= $new_time; } } } sub UNLOAD { write_config(); } Irssi::theme_register([ 'myhl', '{hilight $0} $1 $2', ]); Irssi::command_bind('help', sub { if ($_[0] =~ m/boinc/ ) { cmd_help(); Irssi::signal_stop; } } ); Irssi::signal_add('pidwait', 'sig_read_exec'); Irssi::signal_add('setup changed', 'sig_setup_changed'); Irssi::statusbar_item_register ('boinc_credit', 0, 'sb_boinc_credit'); Irssi::settings_add_int($IRSSI{'name'}, 'boinc_update_cycle', 15); Irssi::settings_add_str($IRSSI{'name'}, 'boinc_command', 'boinccmd'); Irssi::command_bind($IRSSI{name},\&cmd); my @opt=map {$_ =~ s/=.*$//, $_ } keys %options; Irssi::command_set_options($IRSSI{name}, join(" ", @opt)); read_config(); sig_setup_changed();