#!/usr/bin/env perl
#-*- Mode: perl; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*-

# Time configurator. Designed to be architecture- and distribution independent.
#
# Copyright (C) 2000-2001 Ximian, Inc.
#
# Authors: Hans Petter Jansson <hpj@ximian.com>
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU Library General Public License as published
# by the Free Software Foundation; either version 2 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 Library General Public License for more details.
#
# You should have received a copy of the GNU Library General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307, USA.

# Best viewed with 100 columns of width.

# Configuration files affected/used:
#
# /usr/share/zoneinfo/zone.tab
# /etc/ntp.conf
# /etc/ntp/step-tickers
# /etc/localtime

# Running programs affected/used:
#
# date



require "/usr/share/ximian-setup-tools/scripts/general.pl";
require "/usr/share/ximian-setup-tools/scripts/platform.pl";
require "/usr/share/ximian-setup-tools/scripts/util.pl";
require "/usr/share/ximian-setup-tools/scripts/file.pl";
require "/usr/share/ximian-setup-tools/scripts/xml.pl";
require "/usr/share/ximian-setup-tools/scripts/service.pl";


# --- Tool information --- #

$name = "time";
$version = "0.1.0";
@platforms = ("redhat-5.2", "redhat-6.0", "redhat-6.1", "redhat-6.2",
              "redhat-7.0");

$description =<<"end_of_description;";
       Configures your system clock, timezone and time server list.
end_of_description;

$progress_max = 365;


# --- System config file locations --- #

# We list each config file type with as many alternate locations as possible.
# They are tried in array order. First found = used.

@ntp_conf_names =         ( "/etc/ntp.conf" );
@ntp_step_tickers_names = ( "/etc/ntp/step-tickers" );
@zoneinfo_db_names =      ( "/usr/share/zoneinfo/zone.tab", "/usr/local/share/zoneinfo/zone.tab" );


# --- Internal configuration variables --- #

# Configuration is parsed/read to, and printed/written from, these temporary variables.

$cf_year = 0;
$cf_month = 0;
$cf_mday = 0;
$cf_hour = 0;
$cf_minute = 0;
$cf_second = 0;

$cf_timezone = "";

$cf_sync_active = 0;
@cf_servers = ();


# --- Configuration file manipulation --- #

# xntpd style /etc/ntp.conf
#
# <&filtered lines>
# server <server>
# <&filtered lines>
#
# Exists: Red Hat 6.x
#
# Absent:

sub read_ntp_conf
{
  my $ifh;
  local *FILE;
  
  # Find the file.
  
  $ifh = &xst_open_read_from_names(@ntp_conf_names);
  if (not $ifh) { return; }
  *FILE = $ifh;

  # Parse the file.
  
  &xst_report_info(13, "Reading time server list");
  
  while (<FILE>)
  {
    @line = split(/[ \n\r\t]+/, $_);
    if ($line[0] eq "") { shift(@line); }  # Leading whitespace.
    
    if ($line[0] eq "server" && $line[1] ne "127.127.1.0")  # Disregard loopback.
    { &xst_push_unique(\@cf_servers, $line[1]); }
  }
}


sub write_ntp_conf
{
  my ($ifh, $ofh);
  local (*INFILE, *OUTFILE);
  my $wrote_servers = 0;

  # Find the file.

  ($ifh, $ofh) = &xst_open_filter_write_from_names(@ntp_conf_names);
  if (not $ofh) { return; }  # No point if we can't write.
  *INFILE = $ifh; *OUTFILE = $ofh;

  # Write the file, preserving as much as possible from INFILE.

  &xst_report_info(11, "Saving time server list");

  while (<INFILE>)
  {
    @line = split(/[ \n\r\t]+/, $_);
    if ($line[0] eq "") { shift(@line); }  # Leading whitespace.

    if ($line[0] eq "server")
    {
      if ($line[1] eq "127.127.1.0") { print OUTFILE; }
      elsif (!$wrote_servers)
      {
        for $elem (@cf_servers) { print OUTFILE "server $elem\n"; }
        $wrote_servers = 1;
      }
    }
    else { print OUTFILE; }
  }
  
  if (!$wrote_servers)
  {
    for $elem (@cf_servers) { print OUTFILE "server $elem\n"; }
    $wrote_servers = 1;
  }
  
  close(OUTFILE);
  if (*INFILE) { close(INFILE); }
}


# Red Hat style /etc/ntp/step-tickers
#
# <server>
# <server>
# ...
#
# This file is used as source for servers from which to sync the system
# clock before xntpd is started. In Red Hat, this is done because xntpd
# refuses to sync the clock if it is too much off.
#
# Exists: Red Hat 6.x
#
# Absent:

sub write_ntp_step_tickers
{
  my $ofh;
  local *FILE;

  # Find the file (it might very well not exist beforehand).

  $ofh = &xst_open_write_from_names(@ntp_step_tickers_names);
  if (not $ofh) { return; }
  *FILE = $ofh;

  # Write the file.

  &xst_report_info(12, "Saving time server preload list");

  for $elem (@cf_servers) { print FILE "$elem\n"; }

  close(FILE);
}


# Red Hat (?) style /etc/localtime
#
# Copied from /usr/share/zoneinfo/Etc/<match>, if found.
# TODO: Otherwise, we generate it ourselves.
#
# Exists: Red Hat 6.x
#
# Absent:

sub write_localtime
{
  my ($tz, $name);
  my $zonebase = "/usr/share/zoneinfo/";

  &xst_report_info(10, "Setting timezone");

  $tz = "$zonebase$cf_timezone";

  if (stat($tz) ne "")
  {
    # Badly needs fixing & porting.

    $name = "/etc/localtime";

    (my $fullname = "$prefix/$name") =~ tr/\//\//s;  # '//' -> '/'
    &xst_report_info(1, "Writing timezone configuration to \"$fullname\"");

    ($name = "$prefix/$name") =~ tr/\//\//s;  # '//' -> '/'
    &xst_create_path_for_file($name);

    # Make a backup if the file already exists - if the user specified a prefix,
    # it might not.

    if (stat($name))
    {
      # NOTE: Might not work everywhere. Might be unsafe if the user is allowed
      # to specify a $name list somehow, in the future.

      &xst_run("cp $name $name.confsave");
    }

    # Replace the timezone info file.

    unlink $name;  # Important, since it might be a symlink.
    &xst_run("cp $tz $name");
  }
}


# --- XML parsing --- #


# Scan XML from standard input to an internal tree.

sub xml_parse
{
  # Scan XML to tree.

  $tree = &xst_xml_scan;

  # Walk the tree recursively and extract configuration parameters.
  # This is the top level - find and enter the "networking" tag.

  while (@$tree)
  {
    if ($$tree[0] eq "time") { &xml_parse_time($$tree[1]); }

    shift @$tree;
    shift @$tree;
  }

  return($tree);
}


# <networking>...</networking>

sub xml_parse_time
{
  my $tree = $_[0];

  shift @$tree;  # Skip attributes.

  while (@$tree)
  {
    if    ($$tree[0] eq "year")     { $cf_year = &xst_xml_get_word($$tree[1]); }
    elsif ($$tree[0] eq "month")    { $cf_month = &xst_xml_get_word($$tree[1]); }
    elsif ($$tree[0] eq "monthday") { $cf_mday = &xst_xml_get_word($$tree[1]); }
    elsif ($$tree[0] eq "hour")     { $cf_hour = &xst_xml_get_word($$tree[1]); }
    elsif ($$tree[0] eq "minute")   { $cf_minute = &xst_xml_get_word($$tree[1]); }
    elsif ($$tree[0] eq "second")   { $cf_second = &xst_xml_get_word($$tree[1]); }
    elsif ($$tree[0] eq "timezone") { $cf_timezone = &xst_xml_get_word($$tree[1]); }
    elsif ($$tree[0] eq "synchronization") { &xml_parse_synchronization($$tree[1]); }

    shift @$tree;
    shift @$tree;
  }
}


sub xml_parse_synchronization
{
  my $tree = $_[0];

  $cf_sync_active = &xst_read_boolean($$tree[0]->{active});
  shift @$tree;

  while (@$tree)
  {
    if ($$tree[0] eq "server")  { &xst_push_unique(\@cf_servers, &xst_xml_get_word($$tree[1])); }
    
    shift @$tree;
    shift @$tree;
  }
}


# --- XML printing --- #


sub xml_print
{
  print "<?xml version='1.0' encoding='ISO-8859-1' standalone='yes'?>\n";
  print "<!DOCTYPE time []>\n\n";
  print "<time>\n";
  &xst_xml_enter ();

  &xst_xml_vspace ();
  &xst_xml_print ("<!-- Configuration starts here -->\n");
  &xst_xml_vspace ();

  &xst_xml_print ("<year>$cf_year</year>\n");
  &xst_xml_print ("<month>$cf_month</month>\n");
  &xst_xml_print ("<monthday>$cf_mday</monthday>\n");
  
  &xst_xml_vspace ();
  
  &xst_xml_print ("<hour>$cf_hour</hour>\n");
  &xst_xml_print ("<minute>$cf_minute</minute>\n");
  &xst_xml_print ("<second>$cf_second</second>\n");

  &xst_xml_vspace ();
  &xst_xml_print ("<timezone>$cf_timezone</timezone>\n");
  &xst_xml_vspace ();
  
  &xst_xml_vspace ();
  &xst_xml_print ("<synchronization active='", &xst_print_boolean_yesno($cf_sync_active), "'>\n");
  
  &xst_xml_enter ();
  
  for $server (@cf_servers)
  {
    &xst_xml_print ("<server>$server</server>\n");
  }
  
  &xst_xml_leave ();
  
  &xst_xml_print ("</synchronization>\n");
  &xst_xml_vspace ();
  
  &xst_xml_print ("<!-- End of configuration -->\n");
  &xst_xml_vspace ();

  &xst_xml_leave ();
  print "</time>\n";
}


# --- Get (read) config --- #


sub get_zone
{
  local *TZLIST;
  my $zone;
  my $diff_tool;
  my $size_search;
  my $size_test;

  *TZLIST = &xst_open_read_from_names(@zoneinfo_db_names);
  if (not *TZLIST) { return; }

  $diff_tool = &xst_locate_tool("diff");

  &xst_report_info(8, "Scanning timezones");

  # Get the filesize for /etc/localtime so that we don't have to execute
  # a diff for every file, only for file with the correct size. This speeds
  # up loading 
  $size_search = (stat("/etc/localtime"))[7];

  while (<TZLIST>)
  {
    if (/#/) { next; }                     # Skip comments.
    ($d, $d, $zone) = split /[\t ]+/, $_;  # Get 3rd column.
    chomp $zone;                           # Remove linefeeds.


    # See if this zone file matches the installed one.
    &xst_report_info(9, "Scanning timezones: $zone");
    &xst_print_progress();

    $size_test = (stat("/usr/share/zoneinfo/$zone"))[7];
    if ($size_test eq $size_search)
    {
      if (!system "$diff_tool /usr/share/zoneinfo/$zone /etc/localtime >/dev/null 2>/dev/null")
      {
        # Found a match.
        last;
      }
    }
    
    $zone = "";
  }
  
  $cf_timezone = $zone;
  close (TZLIST);
}


sub get_time
{
  my $datetext;
  
  &xst_report_info(7, "Querying system clock");
  
  $datetext = `date +%Y.%m.%d.%H:%M.%S.%z`;
  
  ($cf_year, $cf_month, $cf_mday, $cf_hour, $cf_minute, $cf_second) =
    ($datetext =~ /^([0-9]+).([0-9]+).([0-9]+).([0-9]+):([0-9]+).([0-9]+)/);

  # TODO: We should fall back to internal Perl functions here.
}


sub check_servers
{
  if (-f "/etc/rc.d/init.d/xntpd")
  {
    if (!system "/etc/rc.d/init.d/xntpd status >/dev/null 2>/dev/null")
    {
      $cf_sync_active = 1;
      &xst_report_info(2, "Found XNTPD enabled");
    }
    else
    {
      $cf_sync_active = 0;
      &xst_report_info(3, "Found XNTPD disabled");
    }
  }
  else
  {
    &xst_report_warning(1, "Could not find a way to check XNTPD status");
  }
}


sub get
{
  &get_zone ();
  &read_ntp_conf (); &xst_print_progress ();
  &get_time (); &xst_print_progress ();
  &check_servers (); &xst_print_progress ();

  &xst_end();
  &xml_print ();
}


# --- Set (write) config --- #


sub set_time
{
  &xst_report_info(6, "Setting system clock");

  &xst_run(sprintf("date %02d%02d%02d%02d%04d.%02d >/dev/null 2>/dev/null",
                 $cf_month, $cf_mday, $cf_hour, $cf_minute, $cf_year, $cf_second));
}


sub restart_servers
{
  if ($cf_sync_active)
  {
    &xst_report_info(4, "Restarting XNTPD with new configuration");
    &xst_service_restart(81, "", "xntpd");
  }
  else
  {
    &xst_report_info(5, "Stopping XNTPD");
    &xst_service_disable(15, "", "xntpd");
  }
}


sub set
{
  &xml_parse ();

  &write_ntp_conf (); &xst_progress (10);
  &write_ntp_step_tickers (); &xst_progress (20);
  &write_localtime (); &xst_progress (30);

  if ($xst_do_immediate)
  {
    &set_time; &xst_progress (50);
    &restart_servers; &xst_progress (80);
  }
  
  &xst_end();
}


# --- Filter config: XML in, XML out --- #


sub filter
{
  &xml_parse ();
  &xst_end();
  &xml_print ();
}


# --- Main --- #

&xst_init($name, $version, $description, @ARGV);
&xst_platform_ensure_supported (@platforms);

# Do our thing.

if    ($xst_operation eq "get")    { &get; }
elsif ($xst_operation eq "set")    { &set; }
elsif ($xst_operation eq "filter") { &filter; }
