# -*-perl-*-

# $Id$

# $Log$
# Revision 1.8  2011/08/30 19:59:48  koczan
# System calls shouldn't be PATH-dependent
#
# Revision 1.7  2010/08/05 16:59:58  koczan
# 3 threads instead of 2
# added a cleanup step and label
#
# Revision 1.6  2008/01/31 17:32:43  thomas
# Need Compress::Zlib to uncompress offline restores [CSL #339211]
#
# Revision 1.5  2007/02/28 21:20:14  thomas
# Multiple simultaneous writer enhancement
#
# Revision 1.4  2002/08/14 17:13:08  thomas
# 0.2.9 release
#
# Revision 1.3  2002/07/23 16:02:29  thomas
# version 0.2.5
#
# Revision 1.2  2002/07/10 14:01:29  thomas
# sushi-0.2.4
#
# Revision 1.1  2002/07/08 15:56:36  thomas
# 0.2.3 release
#

package Sushi::OffLine::Disk;

use strict;
use Data::Dumper;
use DirHandle;
use FileHandle;
# Needed to uncompress offline restores [CSL #339211]
use Compress::Zlib;
use POSIX ":sys_wait_h";

use base qw(Sushi::OffLine);

# This file provides the following methods, used to access offline
# storage devices

###############################################################################
# The old foreground 'copy an epoch' method.  Kept for the histoical record.
# This code will no longer work without modification.

sub copy {
  my $self = shift;
  my $epoch = shift;

  # Make sure the destination disk is mounted...
  if (!defined $self->{mountpoint}) {
    $self->prepare or return 0;
  }

  my $mtpt;
  my $retval = 0; # Assume failure

  my $reason = "Offline: $epoch->{schedule_group}:$epoch->{type}:$epoch->{name} for $self->{storage_id}";
  if (!defined ($mtpt = Sushi::AfsBits::mountVolume ($epoch->{afs_volume}, $reason))) {
    $self->error ("Mount of result volume $epoch->{afs_volume} failed.  Bummer.\n");
    return 0;
  }
  Sushi::Misc::runCmd ('/bin/mkdir', '-p', "$self->{mountpoint}/$epoch->{afs_path}");
  if (! -d "$self->{mountpoint}/$epoch->{afs_path}") {
    $self->error ("mkdir $self->{mountpoint}/$epoch->{afs_path} failed.  Bummer.\n");
    goto cleanup;
  }
  my @files;
  @files = glob "$self->{mountpoint}/$epoch->{afs_path}/data*";
  unlink @files;
  @files = glob "$mtpt/$epoch->{afs_path}/data*";
  if (!@files) {
    $self->error ("$mtpt/$epoch->{afs_path} empty!  Bummer.\n");
    goto cleanup;
  }
  if (Sushi::Misc::runCmd ('/bin/cp', @files, "$self->{mountpoint}/$epoch->{afs_path}/")) {
    $self->error ("Data copy to $self->{mountpoint}/$epoch->{afs_path} failed.  Bummer.\n");
    goto cleanup;
  }

 succeed:
  $retval = 1;

 cleanup:
  if (!Sushi::AfsBits::unMountVolume (($epoch->{afs_volume}, $reason))) {
    $self->error ("Unmount of result volume $epoch->{afs_volume} failed.  Continuing...\n");
  }
  return $retval;
}


###############################################################################
# Do any preprocessing needed.  For this type, mount the disk.

sub prepare {
  my $self = shift;

  defined $self->{mountpoint} and return 1;

  # See if the disk is already mounted
  my $fh = new FileHandle "mount|";
  if (!defined $fh) {
    $self->error ("Offline::Disk::prepare failed to run mount command!\n");
    return 0;
  }
  my $mtpt = "/s/sushi/mountpoints/offline.$self->{storage_id}";
  while (<$fh>) {
    if (/on $mtpt/) {
      $self->log (2, "Offline::Disk::prepare $self->{storage_id} already mounted\n");
      $self->{mountpoint} = $mtpt;
      return 1;
    }
  }

  # Try to mount the disk
  if (! -d $mtpt) {
    if (Sushi::Misc::runCmd ('/bin/mkdir', $mtpt)) {
      $self->error ("mkdir of mount point $mtpt failed.  Bummer.\n");
      return 0;
    }
  }
  if (Sushi::Misc::runCmd ('/bin/mount', "LABEL=$self->{storage_id}", $mtpt)) {
    $self->error ("Mount of off-line volume $self->{storage_id} failed.  Bummer.\n");
    return 0;
  }
  $self->{mountpoint} = $mtpt;

  return 1;
}

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

sub close {
  my $self = shift;

  defined $self->{mountpoint} or return 1;

  if (Sushi::Misc::runCmd ('/bin/umount', $self->{mountpoint})) {
    $self->error ("Umount of $self->{storage_id} failed.  Bummer.\n");
    return 0;
  }
  if (Sushi::Misc::runCmd ('/bin/rmdir', $self->{mountpoint})) {
    $self->error ("rmdir of mount point $self->{mountpoint} failed.  Bummer.\n");
    return 0;
  }
  delete $self->{mountpoint};
  return 1;
}

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

sub getInfo {
  my $self = shift;
  my $epoch = shift;

  $epoch->{afs_path};
}

###############################################################################
# General signal handling code...
my $signalled = 0;
sub siggobyebyehandler {
  my $sig = shift;
  $sig eq 'PIPE' or print STDERR "Caught SIG$sig: shutting down.\n";
  $signalled = 1;
  return;
}

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

# Signal handling sucks...
# If we receive a SIGPIPE, the runCmd calls to unmount the device and
# remove the mount point (in $self->close) will fail if the default signal
# handlers are restored before $self->close is called.  So, if we get
# signalled, close the handler and exit the program
# (before the handlers get restored by exitting this routine).

sub doRestore {
  my $self = shift;
  my $offline = shift;

  local $SIG{INT} = \&siggobyebyehandler;
  local $SIG{QUIT} = \&siggobyebyehandler;
  local $SIG{HUP}  = \&siggobyebyehandler;
  local $SIG{PIPE} = \&siggobyebyehandler;

  my $path = "$self->{mountpoint}/$offline->{storage_info}";
  my $fn = 0;
  my ($buffer, $nr, $gz);
  {
    while (defined ($gz = gzopen("$path/data$fn.gz", "r")) && !$signalled) {
      while (($nr = $gz->gzread($buffer))>0 && !$signalled) {
	print $buffer;
      }
      $fn++;
    }
  }

  if ($signalled) {
    if (!$self->close) {
      $::log->error ("Closing media device $self->{storage_id} failed.\n");
    }
    exit 0;
  }

  return $fn;
}

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

sub clearmedia {
  my $self = shift;
  my $offline = shift;

  my $res;

  # Assume that the media is already mounted, and do the deed
  $res = Sushi::Misc::runCmd (
			      "/bin/rm",
			      "-r",
			      "$self->{mountpoint}/$offline->{storage_info}"
			     );
  $res and $::log->error ("rm command exited with code $res!\n");
  return ($res ? 0 : 1);
}


##############################################################################
# Start a copy operation in the background
# Return 1 for OK start, 0 for failure
# Mount/unmount of the source volume is now handled in Offline.pm

sub start_copy {
  my $self = shift;   # media instance
  my $epoch = shift;  # atom to copy

  # Make sure the destination disk is mounted...
  if (!defined $self->{mountpoint}) {
    $self->prepare or return 0;
  }

  my ($mtpt, $pid);
  my $retval = 0; # Assume failure

  Sushi::Misc::runCmd ('/bin/mkdir', '-p', "$self->{mountpoint}/$epoch->{afs_path}");
  if (! -d "$self->{mountpoint}/$epoch->{afs_path}") {
    $self->error ("mkdir $self->{mountpoint}/$epoch->{afs_path} failed.  Bummer.\n");
    goto fail_cleanup;
  }
  my @files;
  @files = glob "$self->{mountpoint}/$epoch->{afs_path}/data*";
  unlink @files;
  @files = glob "$epoch->{mount_mtpt}/$epoch->{afs_path}/data*";
  if (!@files) {
    $self->error ("$epoch->{mount_mtpt}/$epoch->{afs_path} empty!  Bummer.\n");
    return 0;
  }
  if (!($pid=$self->exec_background ('cp', @files, "$self->{mountpoint}/$epoch->{afs_path}/"))) {
    $self->error ("Initiate background data copy to $self->{mountpoint}/$epoch->{afs_path} failed.  Bummer.\n");
    goto fail_cleanup;
  }
  $self->{active}->{"$epoch->{schedule_group}:$epoch->{type}:$epoch->{name}"} =
    {
     pid => $pid,
     epoch => $epoch,
    };

 succeed:
  return 1;

 fail_cleanup:
  Sushi::Misc::runCmd('/bin/rm', '-rf', "$self->{mountpoint}/$epoch->{afs_path}");
  if (-e "$self->{mountpoint}/$epoch->{afs_path}") {
    $self->error("Could not clean up data after failure for $self->{mountpoint}/$epoch->{afs_path}.");
  }
  return 0;
}

###############################################################################
# Fork and exec the requested process
# Deal with open file descriptors
sub exec_background {
  my $self = shift;

  $::log->log (2, ref($self) . ":exec_background: Exec " . join (' ', @_) . "\n");

  my $pid = fork;
  # print STDERR "pid=$pid\n";
  if (!defined $pid) {
    # Failed
    $self->error (ref($self) . ":exec_background: fork() failed!\n");
    return undef;
  } elsif ($pid) {
    # Parent
    return $pid;
  } else {
    # Child
    # Close all extraneous fds
    my $i=2; while ($i++ < 256) {
      close ($i);
    }
    exec (@_);
    die ref($self) . ":exec_background: exec() failed!\n";
  }
  # NOTREACHED
  return undef;
}

###############################################################################
# Get the status of a running epoch copy.
# Returns a two-element list ($status, $return_code)
# $status is undef for "unknown epoch" or other error
#            0 for running
#            1 for done
# if done, $return_code is the unix exit value (only returned once)

sub get_status {
  my $self = shift;   # media obj
  my $epoch = shift;

  # Check status on an specific copy
  my $pid = $self->{active}->{"$epoch->{schedule_group}:$epoch->{type}:$epoch->{name}"}->{pid};
  if (!defined $pid) {
    delete $self->{active}->{"$epoch->{schedule_group}:$epoch->{type}:$epoch->{name}"};
    return (undef, undef);
  }

  # See if it's done
  if (waitpid($pid,&WNOHANG)>0) {
    delete $self->{active}->{"$epoch->{schedule_group}:$epoch->{type}:$epoch->{name}"};
    return (1, $?>>8);
  }

  # See if it's running...
  my $count = kill (0, $pid);
  return (0, undef) if $count;

  # It's not running, get rid of the entry
  delete $self->{active}->{"$epoch->{schedule_group}:$epoch->{type}:$epoch->{name}"};
  return (undef, undef);
}


###############################################################################
# Status function.
# Return a list of epochs currently active on $media.

sub get_active {
  my $self = shift;   # media obj

  return map {$self->{active}->{$_}->{epoch}} keys %{$self->{active}};
}

###############################################################################
# Let the media type decide whether or not it can start more epochs
# For now, three epochs per disk active.

sub can_start_more_epochs {
  my $self = shift;

  my @epochs = $self->get_active;
  return 1 if @epochs<3;
  return 0;
}


1;
