#!/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;