#!/usr/bin/perl
#  rcconf
#                   Copyright (c) 1999-2004 kamop
#  $Id: rcconf,v 1.6 2001/01/08 08:34:49 kamop Exp $
# 

=head1 NAME

rcconf - Debian Runlevel configuration tool

=head1 SYNOPSIS

B<rcconf>

=head1 DESCRIPTION

B<Rcconf> allows you to control which services are started when the
system boots up or reboots.  It displays a menu of all the services 
which could be started at boot.  The ones that are configured to
do so are marked and you can toggle individual services on and off.

B<Rrconf> gets a list of services from /etc/init.d and looks in the
/etc/rc?.d directories to determine whether each service is on or off.

If the number(NN of /etc/rc?.d/NNname) is not 20(default), B<rcconf>
saves the service name and the number in /var/lib/rcconf/services so
as to be able to restore the service to its original configuration.

=head1 FILE

=over 8

=item /var/lib/rcconf/services

The service number data file.

=item /var/lib/rcconf/lock

Lock file.

=back

=head1 SEE ALSO

update-rc.d(8)

=head1 AUTHOR

Atsushi KAMOSHIDA <kamop@debian.org>

=cut

use strict;

END {
  &remove_lock();
}

my($UPDATE_RCD_PATH) = "/usr/sbin/update-rc.d";
my($TMP_FILE) = "/tmp/rc-select.$$";

my($ETC_DIR) = "/etc";
my($DATA_DIR) = "/var/lib/rcconf/";

my($NO_EXECUTE) = "";
#$NO_EXECUTE = "1";

if($NO_EXECUTE == 1){
  $DATA_DIR = "/tmp/";
}

my($DEBUG) = 0;
if($ENV{'RCCONF_DEBUG'} ne ''){
  $DEBUG = 1;
}

my($OUTPUT_FILE) = "";
if($ENV{'RCCONF_SAVE'} ne ''){
  if(open(SAVE, "> ".$ENV{'RCCONF_SAVE'})){
    $OUTPUT_FILE = $ENV{'RCCONF_SAVE'};
  }
}

my($DATA_FILE) = $DATA_DIR."services";
my($LOCK_FILE) = "/var/lock/rcconf";
#my($DATA_FILE) = "/tmp/rcconf/services";
#my($LOCK_FILE) = "/tmp/rcconf/lock";

my($GUIDE_FILE) = $DATA_DIR."guide";
if($ENV{'RCCONF_GUIDE'} ne ''){
  $GUIDE_FILE = $ENV{'RCCONF_GUIDE'};
}

my($DEFAULT_RCNUM) = 20;
my(@unselects) = ("\^\.\$", "\^\.\.\$", "\^rc\$", "\^rcS\$", "\^README\$",
                  "\^skeleton\$",
                  ".*\.dpkg-dist\$", ".*\.dpkg-old\$");

my($DIALOG_OPT) = "--title \"rc selecter\" --separate-output --checklist \"\" 20 70 10";
my($DIALOG_BIN) = "/usr/bin/whiptail";
my($DIALOG_SW_ON) = "1";
my($DIALOG_SW_OFF) = "0";
if ( ! -x $DIALOG_BIN ) {
  $DIALOG_BIN = "/usr/bin/dialog";
  $DIALOG_SW_ON = "on";
  $DIALOG_SW_OFF = "off";
  if ( ! -x $DIALOG_BIN ) {
    print "rcconf needs dialog or whiptail.\n";
    exit 1;
  }
}

if($ENV{'DIALOG_OPT'} ne ''){
  $DIALOG_OPT = $ENV{'DIALOG_OPT'};
}
if($ENV{'DIALOG_BIN'} ne ''){
  $DIALOG_BIN = $ENV{'DIALOG_BIN'};
}
if($ENV{'DIALOG_SW_ON'} ne ''){
  $DIALOG_SW_ON = $ENV{'DIALOG_SW_ON'};
}
if($ENV{'DIALOG_SW_OFF'} ne ''){
  $DIALOG_SW_OFF = $ENV{'DIALOG_SW_OFF'};
}

my($guide) = &read_guidefile(file=>$GUIDE_FILE);
&make_lock();
my($rcdf) = &read_rcd_default(root_dir=>$ETC_DIR);
my($data) = &read_data(file=>$DATA_FILE);
my($default) = &select_default(rcdf=>$rcdf, data=>$data);
my($initd) = &read_initd_dir(root_dir=>$ETC_DIR);
$initd = &select_unlinked_initd(initd=>$initd, rcdf=>$rcdf,
                                unselects=>\@unselects);

my($ret, $res);
if(($ARGV[0] ne '') && (-f $ARGV[0])){
  ($ret, $res) = &read_config_file(file=>$ARGV[0]);
  if($ret == 0){
    ($ret, $res) = &set_config(on=>$default, off=>$initd, set=>$res);
  }
}else{
  ($ret, $res) = &output_dialog(on=>$default, off=>$initd, info=>$guide);
}
if($ret == 0){
  my($res_on, $res_off) = &diff_result(on=>$default, off=>$initd, res=>$res);
  &exec_update(on=>$res_on, off=>$res_off, data=>$data);
  &write_data(file=>$DATA_FILE, data=>$data);
}
&remove_lock();
if($OUTPUT_FILE ne ''){
  close(SAVE);
}
exit;

#&print_rcd(rcdf=>$rcdf);

#######################################################################
#######################################################################
#######################################################################

#######################################################################
## MODULE: read_rcd_default
## DESC: Read files in rc?.d(?:=0..6) directory and generate %rcdf.
##         %rcdf->{'package'} -> [0]  service num in rc0.d/S??package
##                               [1]
##                               [2]
##                               [3]
##                               [4]
##                               [5]
##                               [6]
##                               [7]    rcS.d/S??package
##                               [10] service num in rc0.d/K??package
##                               [11]   rc1.d/K??package
##                               [12]   rc2.d/K??package
##                               [13]   rc3.d/K??package
##                               [14]   rc4.d/K??package
##                               [15]   rc5.d/K??package
##                               [16]   rc6.d/K??package
##                               [17]   rcS.d/K??package
## IN:
## OUT:
## OP:
## STATUS:
## END:
sub read_rcd_default{
  my($self) = shift if(defined($_[0]) && (ref($_[0]) ne ''));
  my(%ref) = @_;
##
  my($root_dir) = $ref{'root_dir'};
  my(%rcdf) = ();
  my($i);
  my($dir);
## rc?.d
  my($start, $end);
  for($i = 0; $i <= 6; $i++){
    $dir = $root_dir."/rc".$i.".d";
    ($start, $end) = &read_rc_dir(dir=>$dir);
    &setup_rcd(rcdf=>\%rcdf, rcfile=>$start, dirnum=>$i, margin=>0);
    &setup_rcd(rcdf=>\%rcdf, rcfile=>$end, dirnum=>$i, margin=>10);
  }
## rcS.d
  $dir = $root_dir."/rcS.d";
  ($start, $end) = &read_rc_dir(dir=>$dir);
  &setup_rcd(rcdf=>\%rcdf, rcfile=>$start, dirnum=>7, margin=>0);
  &setup_rcd(rcdf=>\%rcdf, rcfile=>$end, dirnum=>7, margin=>10);
  return(\%rcdf);
} ## read_rcd_default

#######################################################################
## MODULE: read_rc_dir
## DESC: Open directory specified in $dir, and list Start/Stop service
##        @start [0] -> {file} -+- 'num'  service number
##               [1]            +- 'name' file name
## IN:
## OUT:
## OP:
## STATUS:
## END:
sub read_rc_dir{
  my($self) = shift if(defined($_[0]) && (ref($_[0]) ne ''));
  my(%ref) = @_;
##
  my($dir) = $ref{'dir'};
##
  opendir(DIR, $dir) || die "No such directory: $!";
##
  my(@starts) = ();
  my(@stops) = ();
  my(@dirs) = readdir(DIR);
  foreach (@dirs){
    if(/^S([0-9][0-9])(.*)$/){
      push(@starts, &new_file(num=>$1, name=>$2));
      next;
    } ## if
    if(/^K([0-9][0-9])(.*)$/){
      push(@stops, &new_file(num=>$1, name=>$2));
      next;
    } ## if
  } ## while()
  closedir(DIR);
  return(\@starts, \@stops);
} ## read_rc_dir

#######################################################################
## MODULE: read_initd_dir
## DESC: Collect files in inet.d/ directory.
## IN:
## OUT:
## OP:
## STATUS:
## END:
sub read_initd_dir{
  my($self) = shift if(defined($_[0]) && (ref($_[0]) ne ''));
  my(%ref) = @_;
##
  my($root_dir) = $ref{'root_dir'};
##
  my($dir) = $root_dir."/init.d";
  opendir(DIR, $dir) || die "No such directory: $!";
  my(@dirs) = readdir(DIR);
  close(DIR);
##
  return(\@dirs);
} ## read_initd_dir

#######################################################################
## MODULE: select_unlinked_initd
## DESC: Compare between %rcdf and @initd, and list file in inet.d/
##       directory which is not linked to rc?.d.
##       Listed files are not serviced packages.
## IN:
## OUT: \@new_initrd := not serviced packages
## OP:
## STATUS:
## END:
sub select_unlinked_initd{
  my($self) = shift if(defined($_[0]) && (ref($_[0]) ne ''));
  my(%ref) = @_;
##
  my($initd) = $ref{'initd'};
  my($rcdf) = $ref{'rcdf'};
  my($unselects) = $ref{'unselects'};
##
  my(@new_initrd) = ();
  my($key);
  my($unselect);
##
  foreach $key (@{$initd}){
    next if(&check_unselect(file=>$key, unselects=>$unselects));
    if(! exists($rcdf->{$key})){
      push(@new_initrd, $key);
    }
  }
  return(\@new_initrd);
} ## select_unlinked_initd()

#######################################################################
## MODULE: check_unselect
## DESC: Check if 'file' exists in unselects array.
## IN:
## OUT:  results 0 := file is not in the array.
##               1 := file exists in the array.
## OP:
## STATUS:
## END:
sub check_unselect{
  my($self) = shift if(defined($_[0]) && (ref($_[0]) ne ''));
  my(%ref) = @_;
##
  my($file) = $ref{'file'};
  my($unselects) = $ref{'unselects'};
##
  my($unselect);
  foreach $unselect (@{$unselects}){
    return(1) if($file =~ /$unselect/);
  }
  return(0);
} ## check_unselect()

#######################################################################
## MODULE: new_file
## DESC: Generate new package file
##        'num'  => service number
##        'name' => package name(filename)
## IN:
## OUT:
## OP:
## STATUS:
## END:
sub new_file{
  my($self) = shift if(defined($_[0]) && (ref($_[0]) ne ''));
  my(%ref) = @_;
##
  my($new) = {};
  $new->{'num'} = $ref{'num'};
  $new->{'name'} = $ref{'name'};
  return($new);
} ## new_file()

#######################################################################
## MODULE: new_rcd
## DESC:
## IN:
## OUT:
## OP:
## STATUS:
## END:
sub new_rcd{
  my($self) = shift if(defined($_[0]) && (ref($_[0]) ne ''));
  my(%ref) = @_;
##
  my(@rcd) = ();
  my($i);
  for($i = 0; $i <= 7; $i++){
    $rcd[$i] = -1;       ## start
    $rcd[$i + 10] = -1;  ## end
  }
  return(\@rcd);
} ## new_rc()

#######################################################################
## MODULE: setup_rcd
## DESC:
## IN:
## OUT:
## OP:
## STATUS:
## END:
sub setup_rcd{
  my($self) = shift if(defined($_[0]) && (ref($_[0]) ne ''));
  my(%ref) = @_;
##
  my($rcdf) = $ref{'rcdf'};
  my($rcfile) = $ref{'rcfile'};
  my($dirnum) = $ref{'dirnum'};
  my($margin) = $ref{'margin'};
##
  my($file, $package, $num);
  foreach $file (@{$rcfile}){
    $package = $file->{'name'};
    $num = $file->{'num'};
#    print $package." ".$num." $margin $dirnum\n";
    if(! exists($rcdf->{$package})){
      $rcdf->{$package} = &new_rcd();
#      print "Generate ".$package."\n";
    }
    $rcdf->{$package}->[$dirnum+ $margin] = $num;
  } ## foreach
} ## setup_rcd()

#######################################################################
## MODULE: print_rcd
## DESC:
## IN:
## OUT:
## OP:
## STATUS:
## END:
sub print_rcd{
  my($self) = shift if(defined($_[0]) && (ref($_[0]) ne ''));
  my(%ref) = @_;
##
  my($rcdf) = $ref{'rcdf'};
##
  my($i);
  my($package);
  printf "                           Start                     Stop\n";
  printf "%-20s ","Package Name";
  print " 0  1  2  3  4  5  6  S     0  1  2  3  4  5  6  S\n";
  print '-' x 71 ."\n";
  foreach $package (keys(%{$rcdf})){
    printf "%-20s ", $package;
    for($i = 0; $i <= 7; $i++){ 
      printf "%2d ", $rcdf->{$package}->[$i];
    }
    print "   ";
    for($i = 0; $i <= 7; $i++){ 
      printf "%2d ", $rcdf->{$package}->[$i + 10];
    }
    print "\n";
  } ## foreach $package
} ## print_rcd()

#######################################################################
## MODULE: select_default
## DESC:
## IN:
## OUT:
## OP:
## STATUS:
## END:
sub select_default{
  my($self) = shift if(defined($_[0]) && (ref($_[0]) ne ''));
  my(%ref) = @_;
##
  my($rcdf) = $ref{'rcdf'};
  my($data) = $ref{'data'};
##
  my($i);
  my($package);
  my($link);
  my(@select) = ();
  my($start_num, $stop_num);
  foreach $package (keys(%{$rcdf})){
#    print STDERR $package,"\n";
    $link = $rcdf->{$package};
    $start_num = $link->[2];
    $stop_num = $link->[10];
    if(($start_num != -1) && ($stop_num != -1) &&
       ($link->[3] == $start_num) && ($link->[4] == $start_num) &&
       ($link->[5] == $start_num) &&
       ($link->[11] == $stop_num) && ($link->[16] == $stop_num)){
      push(@select, $package);
      if(($start_num != $DEFAULT_RCNUM) || ($stop_num != $DEFAULT_RCNUM)){
        $data->{$package}->{'start'} = $start_num;
        $data->{$package}->{'stop'} = $stop_num;
      }
#      print STDERR $package,"\n";
    }
  } ## foreach
  return(\@select);
} ## select_default()

#######################################################################
## MODULE: output_dialog
## DESC:
## IN:
## OUT:
## OP:
## STATUS:
## END:
sub output_dialog{
  my($self) = shift if(defined($_[0]) && (ref($_[0]) ne ''));
  my(%ref) = @_;
##
  my($on) = $ref{'on'};
  my($off) = $ref{'off'};
  my($info) = $ref{'info'};
##
  my($list) = " ".&generate_list(package=>$on, sw=>$DIALOG_SW_ON,
                                 info=>$info);
  $list .= " ".&generate_list(package=>$off, sw=>$DIALOG_SW_OFF,
                              info=>$info);
  my($exec) = $DIALOG_BIN." ".$DIALOG_OPT.$list;
  ##
#  print STDERR $exec."\n";
#  exit;
  my($ret) = system($exec." 2>$TMP_FILE");
  ## 'dialog' return 0 if exit by pressing 'OK'
  my(@res) = ();
  if($ret == 0){
    open(RES, $TMP_FILE) || die "Exec error:$!";
    while(<RES>){
      chomp;
      push(@res, $_);
    }
    close(RES);
  }
  unlink $TMP_FILE;
  return($ret, \@res);
} ## output_dialog()

#######################################################################
## MODULE: generate_list
## DESC:
## IN:
## OUT:
## OP:
## STATUS:
## END:
sub generate_list{
  my($self) = shift if(defined($_[0]) && (ref($_[0]) ne ''));
  my(%ref) = @_;
##
  my($package) = $ref{'package'};
  my($sw) = $ref{'sw'};
  my($info) = $ref{'info'};
##
  my($list) = '';
  my($key);
  for $key (sort (@{$package})){
    $list .= "\"".$key."\" ";
    if(exists($info->{$key})){
      $list .= "\"".$info->{$key}."\"";
    }else{
      $list .= "\"\"";
    }
    $list .= " \"".$sw."\" ";;
#    $list .= "\"".$key."\" "." \"\" \"".$sw."\" ";
  }
  return($list);
} ## generate_list()

#######################################################################
## MODULE: read_guidefile
## DESC:
## IN:
## OUT:
## OP:
## STATUS:
## END:
sub read_guidefile{
  my($self) = shift if(defined($_[0]) && (ref($_[0]) ne ''));
  my(%ref) = @_;
##
  my($file) = $ref{'file'};
##
  my($guide) = {};
  my($key, $guideline);
  open(IN, $file) || return($guide);
  while(<IN>){
    chomp;
    /^(\S+)\s+(.*)/;
    $key = $1;
    $guideline = $2;
    $guide->{$key} = $guideline;
  } ## while(<IN>)
  return($guide);
} # read_guidefile()

#######################################################################
## MODULE: diff_result
## DESC:
## IN:
## OUT:
## OP:
## STATUS:
## END:
sub diff_result{
  my($self) = shift if(defined($_[0]) && (ref($_[0]) ne ''));
  my(%ref) = @_;
##
  my($on) = $ref{'on'};
  my($off) = $ref{'off'};
  my($res) = $ref{'res'};
##
  my($hash) = {};
  my(@res_on) = ();
  my(@res_off) = ();
  my($key);
  foreach $key (@{$res}){
    $hash->{$key} = "OK";
  }
#  my($hash) = &array2hash(array=>$res, value=>'OK');
  foreach $key (@{$on}){
    if(! exists($hash->{$key})){
#      print "OFF ",$key,"\n";
      push(@res_off, $key);
    }
  }
  foreach $key (@{$off}){
    if(exists($hash->{$key})){
#      print "ON  ",$key,"\n";
      push(@res_on, $key);
    }
  }
  return(\@res_on, \@res_off);
} ## diff_result

#######################################################################
## MODULE: exec_update
## DESC:
## IN:
## OUT:
## OP:
## STATUS:
## END:
sub exec_update{
  my($self) = shift if(defined($_[0]) && (ref($_[0]) ne ''));
  my(%ref) = @_;
##
  my($on) = $ref{'on'};
  my($off) = $ref{'off'};
  my($data) = $ref{'data'};
##
  my($key);
  my($command);
  foreach $key (@{$on}){
    $command = $UPDATE_RCD_PATH." ".$key." defaults";
    if(exists($data->{$key})){
      $command .= " ".$data->{$key}->{'start'}." ".$data->{$key}->{'stop'};
    }
    $command .= " 1>&2";
    if($NO_EXECUTE eq ''){
      if($DEBUG == 1){
        print STDERR $command."\n";
      }
      print SAVE "$key on\n" if($OUTPUT_FILE ne '');
      system($command);
    }else{
      print STDERR $command."\n";
    }
  }
  foreach $key (@{$off}){
    $command = $UPDATE_RCD_PATH." -f ".$key." remove 1>&2";
    if($NO_EXECUTE eq ''){
      if($DEBUG == 1){
        print STDERR $command."\n";
      }
      print SAVE "$key off\n" if($OUTPUT_FILE ne '');
      system($command);
    }else{
      print STDERR $command."\n";
    }
  }
} ## exec_update()

#######################################################################
## MODULE: read_data
## DESC:
## IN:
## OUT:
## OP:
## STATUS:
## END:
sub read_data{
  my($self) = shift if(defined($_[0]) && (ref($_[0]) ne ''));
  my(%ref) = @_;
##
  my($data) = {};
##
  open(IN, $ref{'file'});
##
  while(<IN>){
    next if(/^\#/);
    if(/([0-9][0-9])\s+([0-9][0-9])\s+(\S+)/){
      $data->{$3}->{'start'} = $1;
      $data->{$3}->{'stop'} = $2;
    }elsif(/([0-9][0-9])\s+(\S+)/){
      $data->{$2}->{'start'} = $1;
      $data->{$2}->{'stop'} = $1;
    }
  } ## while(<IN>)
  close(IN);
  return($data);
} ## read_data()

#######################################################################
## MODULE: read_config_file
## DESC:
## IN:
## OUT:
## OP:
## STATUS:
## END:
sub read_config_file{
  my($self) = shift if(defined($_[0]) && (ref($_[0]) ne ''));
  my(%ref) = @_;
##
  my($tag, $value);
  my($data) = {};
  open(IN, $ref{'file'}) || return(1, 0);
  while(<IN>){
#    s/\r?\n?$//;
    chomp;
    next if(/^$/ || /^\#/);
    ($tag, $value) = split(/\s+/);
    $value = ($value =~ /on/i) ? 1 : 0;
    $data->{$tag} = $value;
  }
  return(0, $data);
} ## read_config_file()

#######################################################################
## MODULE: set_config
## DESC:
## IN:
## OUT:
## OP:
## STATUS:
## END:
sub set_config{
  my($self) = shift if(defined($_[0]) && (ref($_[0]) ne ''));
  my(%ref) = @_;
##
  my($on) = $ref{'on'};
  my($off) = $ref{'off'};
  my($set) = $ref{'set'};
##
  my($on_hash) = &array2hash(array=>$on, value=>'1');
  my($off_hash) = &array2hash(array=>$off, value=>'0');
##
  my($key);
  foreach $key (keys(%{$set})){
    if(($set->{$key} == 1) && (exists($off_hash->{$key}))){
      $on_hash->{$key} = '1';
    }elsif(($set->{$key} == 0) && (exists($on_hash->{$key}))){
      $on_hash->{$key} = '0';
    }
  }
  my(@res) = ();
  foreach $key (keys(%{$on_hash})){
    push(@res, $key) if($on_hash->{$key} == 1);
  }
  return(0, \@res);
} ## set_config()

sub array2hash{
  my($self) = shift if(defined($_[0]) && (ref($_[0]) ne ''));
  my(%ref) = @_;
##
  my($array) = $ref{'array'};
  my($value) = $ref{'value'};
##
  my($key);
  my($hash) = {};
  foreach $key (@{$array}){
    $hash->{$key} = $value;
  }
  return($hash);
} ## array2hash()

#######################################################################
## MODULE: write_data
## DESC:
## IN:
## OUT:
## OP:
## STATUS:
## END:
sub write_data{
  my($self) = shift if(defined($_[0]) && (ref($_[0]) ne ''));
  my(%ref) = @_;
##
  my($file) = $ref{'file'};
  my($data) = $ref{'data'};
  open(OUT, "> ".$file) || die "Cannot write file $file: $!";
  my($key);
  foreach $key (keys(%{$data})){
    print OUT $data->{$key}->{'start'}." ".
      $data->{$key}->{'stop'}." ".
        $key."\n";
  }
  close(OUT);
} ## write_data()

#######################################################################
## MODULE: make_lock
## DESC:
## IN:
## OUT:
## OP:
## STATUS:
## END:
sub make_lock{
  my($self) = shift if(defined($_[0]) && (ref($_[0]) ne ''));
  my(%ref) = @_;
##
  if(-f $LOCK_FILE){
    die "Another rcconf is running, or still remain lock file($LOCK_FILE).";
  }
  open(LOCK, "> ".$LOCK_FILE);
  close(LOCK);
} ## make_lock()

#######################################################################
## MODULE: remove_lock
## DESC:
## IN:
## OUT:
## OP:
## STATUS:
## END:
sub remove_lock{
  my($self) = shift if(defined($_[0]) && (ref($_[0]) ne ''));
  my(%ref) = @_;
##
  unlink($LOCK_FILE);
} ## remove_lock
