2 # Copyright (c) 2010 Zmanda, Inc. All Rights Reserved.
4 # This program is free software; you can redistribute it and/or modify it
5 # under the terms of the GNU General Public License version 2 as published
6 # by the Free Software Foundation.
8 # This program is distributed in the hope that it will be useful, but
9 # WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
10 # or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
13 # You should have received a copy of the GNU General Public License along
14 # with this program; if not, write to the Free Software Foundation, Inc.,
15 # 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
17 # Contact information: Zmanda Inc., 465 S. Mathilda Ave., Suite 300
18 # Sunnyvale, CA 94086, USA, or: http://www.zmanda.com
20 use lib '@amperldir@';
27 package main::Interactive;
28 use base 'Amanda::Interactive';
29 use Amanda::Util qw( weaken_ref );
32 use Amanda::Debug qw( debug );
33 use Amanda::Config qw( :getconf );
34 use Amanda::Recovery::Scan qw( $DEFAULT_CHANGER );
41 clientservice => $params{'clientservice'},
44 # (weak ref here to eliminate reference loop)
45 weaken_ref($self->{'clientservice'});
47 return bless ($self, $class);
53 debug("ignoring spurious Amanda::Recovery::Scan abort call");
61 my $steps = define_steps
62 cb_ref => \$params{'finished_cb'};
64 step send_message => sub {
66 $self->{'clientservice'}->sendmessage("$params{err}");
69 $steps->{'check_fe_feedme'}->();
72 step check_fe_feedme => sub {
73 # note that fe_amrecover_FEEDME implies fe_amrecover_splits
74 if (!$self->{'clientservice'}->{'their_features'}->has(
75 $Amanda::Feature::fe_amrecover_FEEDME)) {
76 return $params{'finished_cb'}->("remote cannot prompt for volumes", undef);
78 $steps->{'send_feedme'}->();
81 step send_feedme => sub {
82 $self->{'clientservice'}->sendctlline("FEEDME $params{label}\r\n", $steps->{'read_response'});
85 step read_response => sub {
86 my ($err, $written) = @_;
87 return $params{'finished_cb'}->($err, undef) if $err;
89 $self->{'clientservice'}->getline_async(
90 $self->{'clientservice'}->{'ctl_stream'}, $steps->{'got_response'});
93 step got_response => sub {
94 my ($err, $line) = @_;
95 return $params{'finished_cb'}->($err, undef) if $err;
97 if ($line eq "OK\r\n") {
98 return $params{'finished_cb'}->(undef, undef); # carry on as you were
99 } elsif ($line =~ /^TAPE (.*)\r\n$/) {
101 if ($tape eq getconf($CNF_AMRECOVER_CHANGER)) {
102 $tape = $Amanda::Recovery::Scan::DEFAULT_CHANGER;
104 return $params{'finished_cb'}->(undef, $tape); # use this device
106 return $params{'finished_cb'}->("got invalid response from remote", undef);
112 # Clerk Feedback class
114 package main::Feedback;
115 use Amanda::Recovery::Clerk;
116 use Amanda::Util qw( weaken_ref );
117 use base 'Amanda::Recovery::Clerk::Feedback';
124 clientservice => $params{'clientservice'}
127 # (weak ref here to eliminate reference loop)
128 weaken_ref($self->{'clientservice'});
136 my ($label, $filenum, $hdr) = @_;
137 $self->{'clientservice'}->sendmessage("restoring part $hdr->{'partnum'} " .
138 "from '$label' file $filenum");
144 my ($holding_file, $hdr) = @_;
145 $self->{'clientservice'}->sendmessage("restoring from holding " .
146 "file $holding_file");
150 # ClientService class
152 package main::ClientService;
153 use base 'Amanda::ClientService';
155 use Amanda::Debug qw( debug info warning );
156 use Amanda::Util qw( :constants );
158 use Amanda::Config qw( :init :getconf );
160 use Amanda::Recovery::Scan;
161 use Amanda::Xfer qw( :constants );
163 use Amanda::Recovery::Clerk;
164 use Amanda::Recovery::Planner;
165 use Amanda::Recovery::Scan;
166 use Amanda::DB::Catalog;
168 # Note that this class performs its control IO synchronously. This is adequate
169 # for this service, as it never receives unsolicited input from the remote
175 $self->{'my_features'} = Amanda::Feature::Set->mine();
176 $self->{'their_features'} = Amanda::Feature::Set->old();
178 $self->setup_streams();
184 # get started checking security for inetd or processing the REQ/REP
186 if ($self->from_inetd()) {
187 if (!$self->check_inetd_security('main')) {
188 $main::exit_status = 1;
189 return $self->quit();
191 $self->{'ctl_stream'} = 'main';
192 $self->{'data_stream'} = undef; # no data stream yet
194 my $req = $self->get_req();
196 # make some sanity checks
198 if (defined $req->{'options'}{'auth'} and defined $self->amandad_auth()
199 and $req->{'options'}{'auth'} ne $self->amandad_auth()) {
200 my $reqauth = $req->{'options'}{'auth'};
201 my $amauth = $self->amandad_auth();
202 push @$errors, "recover program requested auth '$reqauth', " .
203 "but amandad is using auth '$amauth'";
204 $main::exit_status = 1;
207 # and pull out the features, if given
208 if (defined($req->{'features'})) {
209 $self->{'their_features'} = $req->{'features'};
212 $self->send_rep(['CTL' => 'rw', 'DATA' => 'w'], $errors);
213 return $self->quit() if (@$errors);
215 $self->{'ctl_stream'} = 'CTL';
216 $self->{'data_stream'} = 'DATA';
219 $self->read_command();
224 my $ctl_stream = $self->{'ctl_stream'};
225 my $command = $self->{'command'} = {};
227 my @known_commands = qw(
228 HOST DISK DATESTAMP LABEL DEVICE FSF HEADER
231 $_ = $self->getline($ctl_stream);
237 if (/^([A-Z]+)(=(.*))?$/) {
238 my ($cmd, $val) = ($1, $3);
239 if (!grep { $_ eq $cmd } @known_commands) {
240 $self->sendmessage("invalid command '$cmd'");
241 return $self->quit();
243 if (exists $command->{$cmd}) {
244 warning("got duplicate command key '$cmd' from remote");
246 $command->{$cmd} = $val || 1;
250 # features are handled specially. This is pretty weird!
253 my $featurestr = $self->{'my_features'}->as_string();
254 if ($self->from_amandad) {
255 $featreply = "FEATURES=$featurestr\r\n";
257 $featreply = $featurestr;
260 $self->senddata($ctl_stream, $featreply);
264 # process some info from the command
265 if ($command->{'FEATURES'}) {
266 $self->{'their_features'} = Amanda::Feature::Set->from_string($command->{'FEATURES'});
269 if ($command->{'CONFIG'}) {
270 config_init($CONFIG_INIT_EXPLICIT_NAME, $command->{'CONFIG'});
271 my ($cfgerr_level, @cfgerr_errors) = config_errors();
272 if ($cfgerr_level >= $CFGERR_ERRORS) {
273 die "configuration errors; aborting connection";
275 Amanda::Util::finish_setup($RUNNING_AS_DUMPUSER_PREFERRED);
278 $self->setup_data_stream();
281 sub setup_data_stream {
284 # if we're using amandad, then this is ready to roll - it's only inetd mode
285 # that we need to fix
286 if ($self->from_inetd()) {
287 if ($self->{'their_features'}->has($Amanda::Feature::fe_recover_splits)) {
288 # remote side is expecting CONNECT
289 my $port = $self->connection_listen('DATA', 0);
290 $self->senddata($self->{'ctl_stream'}, "CONNECT $port\n");
291 $self->connection_accept('DATA', 30, sub { $self->got_connection(@_); });
293 $self->{'ctl_stream'} = undef; # don't use this for ctl anymore
294 $self->{'data_stream'} = 'main';
307 $self->sendmessage("$err");
308 return $self->quit();
311 if (!$self->check_inetd_security('DATA')) {
312 $main::exit_status = 1;
313 return $self->quit();
315 $self->{'data_stream'} = 'DATA';
323 # put together a dumpspec
325 if (exists $self->{'command'}{'HOST'}
326 || exists $self->{'command'}{'DISK'}
327 || exists $self->{'command'}{'DATESTAMP'}) {
328 my $disk = $self->{'command'}{'DISK'};
329 if (!$self->{'their_features'}->has($Amanda::Feature::fe_amrecover_correct_disk_quoting)) {
330 debug("ignoring specified DISK, as it may be badly quoted");
333 $spec = Amanda::Cmdline::dumpspec_t->new(
334 $self->{'command'}{'HOST'},
336 $self->{'command'}{'DATESTAMP'},
337 undef); # amidxtaped protocol does not provide a level (!?)
340 # figure out if this is a holding-disk recovery
342 if (!exists $self->{'command'}{'LABEL'} and exists $self->{'command'}{'DEVICE'}) {
348 # for holding, give the clerk a null; it won't touch it
349 $chg = Amanda::Changer->new("chg-null:");
351 # if not doing a holding-disk recovery, then we will need a changer.
352 # If we're using the "default" changer, instantiate that. There are
353 # several ways the user can specify the default changer:
355 if (!exists $self->{'command'}{'DEVICE'}) {
357 } elsif ($self->{'command'}{'DEVICE'} eq getconf($CNF_AMRECOVER_CHANGER)) {
362 $chg = Amanda::Changer->new();
364 $chg = Amanda::Changer->new($self->{'command'}{'DEVICE'});
367 # if we got a bogus changer, log it to the debug log, but allow the
368 # scan algorithm to find a good one later.
369 if ($chg->isa("Amanda::Changer::Error")) {
371 $chg = Amanda::Changer->new("chg-null:");
374 my $inter = main::Interactive->new(clientservice => $self);
376 my $scan = Amanda::Recovery::Scan->new(
378 interactive => $inter);
380 $scan->{'scan_conf'}->{'driveinuse'} = Amanda::Recovery::Scan::SCAN_ASK;
381 $scan->{'scan_conf'}->{'volinuse'} = Amanda::Recovery::Scan::SCAN_ASK;
382 $scan->{'scan_conf'}->{'notfound'} = Amanda::Recovery::Scan::SCAN_ASK;
384 $self->{'clerk'} = Amanda::Recovery::Clerk->new(
385 feedback => main::Feedback->new($chg, undef),
389 # if this is a holding recovery, then the plan is pretty easy. The holding
390 # file is given to us in the aptly-named DEVICE command key, with a :0 suffix
391 my $holding_file_tapespec = $self->{'command'}{'DEVICE'};
392 my $holding_file = $self->tapespec_to_holding($holding_file_tapespec);
394 return Amanda::Recovery::Planner::make_plan(
395 holding_file => $holding_file,
396 $spec? (dumpspec => $spec) : (),
397 plan_cb => sub { $self->plan_cb(@_); });
399 my $filelist = Amanda::Util::unmarshal_tapespec($self->{'command'}{'LABEL'});
401 # if LABEL was just a label, then FSF should contain the filenum we want to
403 if ($filelist->[1][0] == 0) {
404 if (exists $self->{'command'}{'FSF'}) {
405 $filelist->[1][0] = 0+$self->{'command'}{'FSF'};
406 # note that if this is a split dump, make_plan will helpfully find the
407 # remaining parts and include them in the restore. Pretty spiffy.
409 # we have only a label and (hopefully) a dumpspec, so let's see if the
410 # catalog can find a dump for us.
411 $filelist = $self->try_to_find_dump(
412 $self->{'command'}{'LABEL'},
415 return $self->quit();
420 return Amanda::Recovery::Planner::make_plan(
421 filelist => $filelist,
422 $spec? (dumpspec => $spec) : (),
423 plan_cb => sub { $self->plan_cb(@_); });
429 my ($err, $plan) = @_;
432 $self->sendmessage("$err");
433 return $self->quit();
436 if (@{$plan->{'dumps'}} > 1) {
437 $self->sendmessage("multiple matching dumps; cannot recover");
438 return $self->quit();
441 if (!$self->{'their_features'}->has($Amanda::Feature::fe_recover_splits)) {
442 # if we have greater than one volume, we may need to prompt for a new
443 # volume in mid-recovery. Sadly, we have no way to inform the client of
444 # this. In hopes that this will "just work", we just issue a warning.
445 my @vols = $plan->get_volume_list();
446 warning("client does not support split dumps; restore may fail if " .
447 "interaction is necessary");
450 # now set up the transfer
451 $self->{'clerk'}->get_xfer_src(
452 dump => $plan->{'dumps'}[0],
453 xfer_src_cb => sub { $self->xfer_src_cb(@_); });
458 my ($errors, $header, $xfer_src, $directtcp_supported) = @_;
462 $self->sendmessage("$_");
464 return $self->quit();
467 $self->{'xfer_src'} = $xfer_src;
468 $self->{'xfer_src_supports_directtcp'} = $directtcp_supported;
469 $self->{'header'} = $header;
471 debug("recovering from " . $header->summary());
473 # set up any filters that need to be applied, decryption first
475 if ($header->{'encrypted'}) {
476 if ($header->{'srv_encrypt'}) {
478 Amanda::Xfer::Filter::Process->new(
479 [ $header->{'srv_encrypt'}, $header->{'srv_decrypt_opt'} ], 0);
480 } elsif ($header->{'clnt_encrypt'}) {
482 Amanda::Xfer::Filter::Process->new(
483 [ $header->{'clnt_encrypt'}, $header->{'clnt_decrypt_opt'} ], 0);
485 $self->sendmessage("could not decrypt encrypted dump: no program specified");
486 return $self->quit();
489 $header->{'encrypted'} = 0;
490 $header->{'srv_encrypt'} = '';
491 $header->{'srv_decrypt_opt'} = '';
492 $header->{'clnt_encrypt'} = '';
493 $header->{'clnt_decrypt_opt'} = '';
494 $header->{'encrypt_suffix'} = 'N';
497 if ($header->{'compressed'}) {
498 # need to uncompress this file
499 debug("..with decompression applied");
501 if ($header->{'srvcompprog'}) {
502 # TODO: this assumes that srvcompprog takes "-d" to decrypt
504 Amanda::Xfer::Filter::Process->new(
505 [ $header->{'srvcompprog'}, "-d" ], 0);
506 } elsif ($header->{'clntcompprog'}) {
507 # TODO: this assumes that clntcompprog takes "-d" to decrypt
509 Amanda::Xfer::Filter::Process->new(
510 [ $header->{'clntcompprog'}, "-d" ], 0);
513 Amanda::Xfer::Filter::Process->new(
514 [ $Amanda::Constants::UNCOMPRESS_PATH,
515 $Amanda::Constants::UNCOMPRESS_OPT ], 0);
519 $header->{'compressed'} = 0;
520 $header->{'uncompress_cmd'} = '';
522 $self->{'xfer_filters'} = [ @filters ];
524 # only send the header if requested
525 if ($self->{'command'}{'HEADER'}) {
526 $self->send_header();
528 $self->expect_datapath();
535 my $header = $self->{'header'};
537 # filter out some things the remote might not be able to process
538 if (!$self->{'their_features'}->has($Amanda::Feature::fe_amrecover_dle_in_header)) {
539 $header->{'dle_str'} = undef;
541 if (!$self->{'their_features'}->has($Amanda::Feature::fe_amrecover_origsize_in_header)) {
542 $header->{'orig_size'} = 0;
545 # even with fe_amrecover_splits, amrecover doesn't like F_SPLIT_DUMPFILE.
546 $header->{'type'} = $Amanda::Header::F_DUMPFILE;
548 my $hdr_str = $header->to_string(32768, 32768);
549 Amanda::Util::full_write($self->wfd($self->{'data_stream'}), $hdr_str, length($hdr_str))
550 or die "writing to $self->{data_stream}: $!";
552 $self->expect_datapath();
555 sub expect_datapath {
558 $self->{'datapath'} = 'none';
560 # short-circuit this if amrecover doesn't support datapaths
561 if (!$self->{'their_features'}->has($Amanda::Feature::fe_amidxtaped_datapath)) {
562 return $self->start_xfer();
565 my $line = $self->getline($self->{'ctl_stream'});
566 if ($line eq "ABORT\r\n") {
567 return Amanda::MainLoop::quit();
569 my ($dpspec) = ($line =~ /^AVAIL-DATAPATH (.*)\r\n$/);
570 die "bad AVAIL-DATAPATH line" unless $dpspec;
571 my @avail_dps = split / /, $dpspec;
573 if (grep /^DIRECT-TCP$/, @avail_dps) {
574 # remote can handle a directtcp transfer .. can we?
575 if ($self->{'xfer_src_supports_directtcp'}) {
576 $self->{'datapath'} = 'directtcp';
578 $self->{'datapath'} = 'amanda';
581 # remote can at least handle AMANDA
582 die "remote cannot handle AMANDA datapath??"
583 unless grep /^AMANDA$/, @avail_dps;
584 $self->{'datapath'} = 'amanda';
593 # create the appropriate destination based on our datapath
595 if ($self->{'datapath'} eq 'directtcp') {
596 $xfer_dest = Amanda::Xfer::Dest::DirectTCPListen->new();
598 $xfer_dest = Amanda::Xfer::Dest::Fd->new(
599 $self->wfd($self->{'data_stream'})),
602 if ($self->{'datapath'} eq 'amanda') {
603 $self->sendctlline("USE-DATAPATH AMANDA\r\n");
604 my $dpline = $self->getline($self->{'ctl_stream'});
605 if ($dpline ne "DATAPATH-OK\r\n") {
606 die "expected DATAPATH-OK";
610 # create and start the transfer
611 $self->{'xfer'} = Amanda::Xfer->new([
613 @{$self->{'xfer_filters'}},
616 $self->{'xfer'}->start(sub { $self->handle_xmsg(@_); });
617 debug("started xfer; datapath=$self->{datapath}");
619 # send the data-path response, if we have a datapath
620 if ($self->{'datapath'} eq 'directtcp') {
621 my $addrs = $xfer_dest->get_addrs();
622 $addrs = [ map { $_->[0] . ":" . $_->[1] } @$addrs ];
623 $addrs = join(" ", @$addrs);
624 $self->sendctlline("USE-DATAPATH DIRECT-TCP $addrs\r\n");
625 my $dpline = $self->getline($self->{'ctl_stream'});
626 if ($dpline ne "DATAPATH-OK\r\n") {
627 die "expected DATAPATH-OK";
631 # and let the clerk know
632 $self->{'clerk'}->start_recovery(
633 xfer => $self->{'xfer'},
634 recovery_cb => sub { $self->recovery_cb(@_); });
639 my ($src, $msg, $xfer) = @_;
641 $self->{'clerk'}->handle_xmsg($src, $msg, $xfer);
642 if ($msg->{'elt'} != $self->{'xfer_src'}) {
643 if ($msg->{'type'} == $XMSG_ERROR) {
644 $self->sendmessage("$msg->{message}");
653 debug("recovery complete");
654 if (@{$params{'errors'}}) {
655 for (@{$params{'errors'}}) {
656 $self->sendmessage("$_");
658 return $self->quit();
661 # note that the amidxtaped protocol has no way to indicate successful
662 # completion of a transfer
663 if ($params{'result'} ne 'DONE') {
664 warning("NOTE: transfer failed, but amrecover does not know that");
673 # close the data fd for writing to signal EOF
674 $self->close($self->{'data_stream'}, 'w');
682 if ($self->{'clerk'}) {
683 $self->{'clerk'}->quit(finished_cb => sub {
686 # it's *way* too late to report this to amrecover now!
687 warning("while quitting clerk: $err");
689 Amanda::MainLoop::quit();
692 Amanda::MainLoop::quit();
698 sub check_inetd_security {
702 my $firstline = $self->getline($stream);
703 if ($firstline !~ /^SECURITY (.*)\n/) {
704 warning("did not get security line");
705 print "ERROR did not get security line\r\n";
709 my $errmsg = $self->check_bsd_security($stream, $1);
711 print "ERROR $errmsg\r\n";
723 my $buf = Amanda::Util::full_read($self->rfd('main'), 1024);
727 # we've read main to EOF, so close it
728 $self->close('main', 'r');
730 return $self->{'req'} = $self->parse_req($req_str);
735 my ($streams, $errors) = @_;
738 # first, if there were errors in the REQ, report them
740 for my $err (@$errors) {
741 $rep .= "ERROR $err\n";
744 my $connline = $self->connect_streams(@$streams);
745 $rep .= "$connline\n";
747 # rep needs a empty-line terminator, I think
750 # write the whole rep packet, and close main to signal the end of the packet
751 $self->senddata('main', $rep);
752 $self->close('main', 'w');
755 # helper function to get a line, including the trailing '\n', from a stream. This
756 # reads a character at a time to ensure that no extra characters are consumed. This
757 # could certainly be more efficient! (TODO)
761 my $fd = $self->rfd($stream);
766 POSIX::read($fd, $c, 1)
773 $chopped =~ s/[\r\n]*$//g;
774 debug("CTL << $chopped");
779 # like getline, but async; TODO:
780 # - make all uses of getline async
781 # - use buffering to read more than one character at a time
784 my ($stream, $async_read_cb) = @_;
785 my $fd = $self->rfd($stream);
791 my ($err, $data) = @_;
793 return $async_read_cb->($err, undef) if $err;
796 if ($buf =~ /\r\n$/) {
798 $chopped =~ s/[\r\n]*$//g;
799 debug("CTL << $chopped");
801 $async_read_cb->(undef, $buf);
803 Amanda::MainLoop::async_read(fd => $fd, size => 1, async_read_cb => $data_in);
806 Amanda::MainLoop::async_read(fd => $fd, size => 1, async_read_cb => $data_in);
809 # helper function to write a data to a stream. This does not add newline characters.
810 # If the callback is given, this is async (TODO: all calls should be async)
813 my ($stream, $data, $async_write_cb) = @_;
814 my $fd = $self->wfd($stream);
816 if (defined $async_write_cb) {
817 return Amanda::MainLoop::async_write(
820 async_write_cb => $async_write_cb);
822 Amanda::Util::full_write($fd, $data, length($data))
823 or die "writing to $stream: $!";
827 # send a line on the control stream, or just log it if the ctl stream is gone;
828 # async callback is just like for senddata
831 my ($msg, $async_write_cb) = @_;
834 $chopped =~ s/[\r\n]*$//g;
836 if ($self->{'ctl_stream'}) {
837 debug("CTL >> $chopped");
838 return $self->senddata($self->{'ctl_stream'}, $msg, $async_write_cb);
840 debug("not sending CTL message as CTL is closed >> $chopped");
841 if (defined $async_write_cb) {
842 $async_write_cb->(undef, length($msg));
847 # send a MESSAGE on the CTL stream, but only if the remote has
848 # fe_amrecover_message
853 if ($self->{'their_features'}->has($Amanda::Feature::fe_amrecover_message)) {
854 $self->sendctlline("MESSAGE $msg\r\n");
856 warning("remote does not understand MESSAGE; not sent: MESSAGE $msg");
860 # covert a tapespec to a holding filename
861 sub tapespec_to_holding {
865 my $filelist = Amanda::Util::unmarshal_tapespec($tapespec);
867 # $filelist should have the form [ $holding_file => [ 0 ] ]
868 die "invalid holding tapespec" unless @$filelist == 2;
869 die "invalid holding tapespec" unless @{$filelist->[1]} == 1;
870 die "invalid holding tapespec" unless $filelist->[1][0] == 0;
872 return $filelist->[0];
875 # amrecover didn't give us much to go on, but see if we can find a dump that
876 # will make it happy.
877 sub try_to_find_dump {
879 my ($label, $spec) = @_;
881 # search the catalog; get_dumps cannot search by labels, so we have to use
883 my @parts = Amanda::DB::Catalog::get_parts(
885 dumpspecs => [ $spec ]);
888 $self->sendmessage("could not find any matching dumps on volume '$label'");
892 # (note that if there is more than one dump in @parts, the planner will
895 # sort the parts by their order on each volume. This sorts the volumes
896 # lexically by label, but the planner will straighten it out.
897 @parts = Amanda::DB::Catalog::sort_dumps([ "label", "filenum" ], @parts);
899 # loop over the parts for the dump and make a filelist.
901 my $last_filenums = undef;
903 for my $part (@parts) {
904 next unless defined $part; # skip part number 0
905 if ($part->{'label'} ne $last_label) {
906 $last_label = $part->{'label'};
908 push @$filelist, $last_label, $last_filenums;
910 push @$last_filenums, $part->{'filenum'};
920 use Amanda::Debug qw( debug );
921 use Amanda::Util qw( :constants );
922 use Amanda::Config qw( :init );
924 our $exit_status = 0;
927 Amanda::Util::setup_application("amidxtaped", "server", $CONTEXT_DAEMON);
928 config_init(0, undef);
929 Amanda::Debug::debug_dup_stderr_to_debug();
931 my $cs = main::ClientService->new();
932 Amanda::MainLoop::call_later(sub { $cs->run(); });
933 Amanda::MainLoop::run();
935 debug("exiting with $exit_status");
936 Amanda::Util::finish_application();