#!/usr/bin/perl -w # Copyright 2007, 2008 Robert ("Bob") Igo of StormLogic, LLC and mythic.tv. # # 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 . package Tweaker; use strict; use Getopt::Lucid qw( :all ); # see http://search.cpan.org/~dagolden/Getopt-Lucid-0.16/lib/Getopt/Lucid.pm for usage details use XML::Twig; # see http://xmltwig.com for usage details use Log::Log4perl qw(get_logger); # see http://search.cpan.org/~mschilli/Log-Log4perl-1.14/lib/Log/Log4perl.pm for usage details # http://www.perl.com/pub/a/2002/09/11/log4perl.html is highly recommended as well use Tweaker::Script; use Tweaker::Definitions; # To install the above modules: # -- # sudo cpan install Getopt::Lucid XML::Twig Log::Log4perl List::Member # choose the defaults for all options, agree to install all dependencies # Copy TweakerDefinitions.pm to /etc/perl use vars qw($debug); use vars qw($interactivity); use vars qw($logfile); use vars qw($twig); my $bottom = Tweaker::Definitions::get_global_variable_value("bottom"); my $optional = Tweaker::Definitions::get_global_variable_value("optional"); my $minimal = Tweaker::Definitions::get_global_variable_value("minimal"); my $null = Tweaker::Definitions::get_global_variable_value("null"); # As each tweak tag is processed, this subroutine is called. Here we will # see if any previous tweak tag had the same name attribute. If so, we will # replace the previous tweak tag with this new tweak tag's contents. sub uniquify_tweaks { my ($twig, $this_tweak) = @_; my $tweak_name = $this_tweak->att('name'); my $previous_tweak = $this_tweak->prev_sibling( qq{tweak[\@name="$tweak_name"]}); my $logger = get_logger('tweaker'); # If the tweak's name is found elsewhere, replace the first # instance with the latest one, then delete the latest one. if ($previous_tweak) { my $log_entry; # Requirement 6.3.1 $log_entry = sprintf("\tReplacing\n\t\t%s\n\twith\n\t\t%s\n", $previous_tweak->sprint, $this_tweak->sprint); $logger->info($log_entry); $this_tweak->cut; $this_tweak->replace($previous_tweak); } } $twig = XML::Twig->new(load_DTD => 1, expand_external_ents => 1, twig_handlers => { 'tweak' => \&uniquify_tweaks } ); # Requirement 4.2 # Print advice on usage and invocation. sub help () { my $logger = get_logger('tweaker'); $logger->fatal("USAGE:\n$0 --tcf file1.tcf [--tcf file2.tcf ...] [--help]"); exit; } sub print_tcf { my(@tweaks) = @_; my $logger = get_logger('tweaker'); $logger->debug(''); # print the XML declaration $logger->debug(''); $logger->debug(''); # then the root element start tag foreach my $tweak (@tweaks) { # the list of tweaks $logger->debug($tweak->sprint); # print the xml content of the element } $logger->debug("\n"); # close the document } # For each tweak, invoke the option that was selected. # Requirement 9.1.5 # Requirement 13 sub invoke_selected_options { my @tweaks= $twig->root->children; # get the tweak list my $logger = get_logger('tweaker'); my $has_selected_option; my $recommendationlevel = $bottom; foreach my $tweak (@tweaks) { # the list of tweaks if ($tweak->att('name')) { $has_selected_option=0; $logger->debug("TWEAK ", $tweak->att('name')); my @options = $tweak->descendants('option'); foreach my $option (@options) { if ($option->first_child('selected')) { $has_selected_option++; $recommendationlevel = $option->first_child('selected')->text; $logger->info("\t", $tweak->att('name'), " : ", $option->att('name'), " is selected (recommendationlevel is ", $recommendationlevel, ")"); # If the selected option is merely $optional, and we are in $minimal interactivity mode, we # really don't know if this option is useful for the user. Just skip it. if (($recommendationlevel == $optional) && ($interactivity eq $minimal)) { # UI Requirement 9.1.4 $logger->debug("\tSKIPPING"); next; } elsif (($tweak->first_child('script')->text) && ($tweak->first_child('script')->text) ne $null) { $logger->debug("\tRUNNING"); my $command = sprintf("%s --implement %s", $tweak->first_child('script')->text, $option->att('name')); $logger->info("\t\tRunning '$command' to run a tweak."); open(COMMAND, "$command|"); while() { # should only be one line of results if ($_) { $logger->debug("script returned text: ", $_); } else { $logger->debug("no return value from script"); } } close(COMMAND); } else { $logger->debug("\tNO SCRIPT TO RUN"); } } } if ($has_selected_option == 0) { $logger->debug("\tNO OPTION SELECTED"); } } } } # Parses an XML file in TCF format, loading its information into memory. # Rely on the DTD to mandate required tags. sub parse_tcf { my $this_tcf = shift(@_); my $logger = get_logger('tweaker'); $logger->info("Parsing $this_tcf..."); $twig->safe_parsefile($this_tcf) || $logger->fatal("Bad TCF $this_tcf: $@"); if ($@) { return 0; } # ??? For Requirement 5.1, we need to catch parse errors here # $twig now has the parsed $this_tcf $logger->info("DONE parsing $this_tcf"); my $root= $twig->root; # get the root of the twig (tcf) my @tweaks= $root->children; # get the tweak list print_tcf(@tweaks) if $debug; return 1; # successfully parsed $this_tcf } sub parse_core_tcf { my ($core_tcf_pathname) = @_; my $logger = get_logger('tweaker'); $logger->debug("core TCF pathname: $core_tcf_pathname"); # Requirement 5.1 if (!(parse_tcf($core_tcf_pathname))) { # Part of Requirement 6.4.1 $logger->fatal("$0 requires at least one valid .tcf file."); $logger->fatal("$core_tcf_pathname, or a .tcf that it includes, did not parse."); exit; } # $twig->root->print; } sub process_parameters () { # Accept these parameters: # -- my @parameters = ( Switch("help")->anycase, # Requirement 4.2 Switch("debug")->anycase, # part of Requirement 8 Param("interactivity")->default($minimal), # side-effect of UI Requirement 9 for v0.7 ); my $opt = Getopt::Lucid->getopt( \@parameters ); my $help = $opt->get_help; $debug = $opt->get_debug; $interactivity = $opt->get_interactivity; if ($interactivity ne $minimal) { # side-effect of UI Requirement 9 for v0.7 my $logger = get_logger('tweaker'); $logger->warn("This version of Tweaker ignores requests for $interactivity interactivity and defaults to $minimal."); $interactivity = $minimal; } if ( $help > 0 ) { help; } } # Requirement 6.6: Tweaker shall determine Recommendation Levels and default Options # Requirements 6.6.1 and 6.6.2 sub post_process_tweaks { my $logger = get_logger('tweaker'); # Requirement 6.6.1 my $root= $twig->root; # get the root of the twig (tcf) my @tweaks= $root->children; # get the tweak list foreach my $tweak (@tweaks) { $logger->debug("#######"); my @options = $tweak->descendants('option'); my $name_of_most_recommended_option=""; my $most_recommended_option; my $highest_recommendationlevel=$bottom; foreach my $option (@options) { $logger->debug("===== OPTION"); $logger->debug($option->sprint); my $recommendationlevel = $option->first_child('recommendationlevel'); if ($recommendationlevel) { # Requirement 6.6.1.1 $logger->debug("\t+++"); $logger->debug("\tUsing predefined recommendation level:", $recommendationlevel->text); } else { # Requirement 6.6.1.4 my $result="optional"; my $explanation=""; if (($tweak->first_child('script')->text) && ($tweak->first_child('script')->text) ne $null) { # Requirement 6.6.1.2 $logger->debug("\t---"); $logger->debug("\tThis has no defined recommendation level."); # Get the name of the script for this tweak and invoke it with the name of the option. my $command = sprintf("%s --poll %s", $tweak->first_child('script')->text, $option->att('name')); $logger->debug("\t\tRunning '$command' to see what it should be."); # Create a recommendationlevel element and populate it with the script's return value. open(COMMAND, "$command|"); while() { # should only be one line of results $logger->debug("Got this: ", $_); if ($_) { chop; my @text = split(/\|/); $result = $text[0]; if ($text[1]) { $explanation = $text[1]; } } } close(COMMAND); } # Requirement 6.6.1.3 $option->set_field ( 'recommendationlevel', $result ); if ($explanation) { $option->set_field ( 'explanation', $explanation ); } #$logger->debug("*** Paste results: ", $option->sprint); } # Requirement 6.6.2 : Auto-select the Option with the highest non-negative Recommendation Level. # Requirement 6.6.2.1 : If there is a tie, Tweaker shall auto-select the first Option with the highest Recommendation Level. $recommendationlevel = $option->first_child('recommendationlevel'); if ($recommendationlevel) { if ($name_of_most_recommended_option) { $logger->debug("comparing ", $name_of_most_recommended_option, "'s recommendationlevel ($highest_recommendationlevel) to ", $recommendationlevel->text, " (",Tweaker::Definitions::get_global_variable_value($recommendationlevel->text),")"); } else { $logger->debug("considering recommendationlevel ", $recommendationlevel->text, " (",Tweaker::Definitions::get_global_variable_value($recommendationlevel->text),")"); } if (($highest_recommendationlevel < Tweaker::Definitions::get_global_variable_value($recommendationlevel->text)) && (Tweaker::Definitions::get_global_variable_value($recommendationlevel->text) >= 0)) { $highest_recommendationlevel = Tweaker::Definitions::get_global_variable_value($recommendationlevel->text); $name_of_most_recommended_option = $option->att('name'); $most_recommended_option = $option; $logger->debug("now recommending: ", $name_of_most_recommended_option, " at level ", $highest_recommendationlevel); } } else { $logger->error("No recommendationlevel defined for ", $option->att('name')); } # Select the best option, based on recommendation level } if ($most_recommended_option) { $logger->debug("(1) BEST OPTION: ", $most_recommended_option->sprint); $most_recommended_option->set_field ( 'selected', $highest_recommendationlevel ); } } } sub main () { my $tweaker_root; if (!($tweaker_root = Tweaker::Script::get_environment_variable("\$TWEAKER_ROOT"))) { Log::Log4perl->easy_init(); my $logger = get_logger(); $logger->fatal("\$TWEAKER_ROOT not defined. Exiting."); exit -1; } my $core_tcf_path = "$tweaker_root/tcf"; my $core_tcf = "tweaker-core.tcf"; my $core_tcf_pathname = "$core_tcf_path/$core_tcf"; my $log4perl_conf = "$tweaker_root/log4perl.conf"; Log::Log4perl::init_and_watch($log4perl_conf,10); process_parameters; parse_core_tcf($core_tcf_pathname); post_process_tweaks; # ??? need to add Requirement 11 here invoke_selected_options; } main; #my @tweaks= $twig->root->children; # get the tweak list #print_tcf(@tweaks) if $debug;