-# Copyright (c) 2005-2008 Zmanda Inc. All Rights Reserved.
+# Copyright (c) 2007, 2008, 2009, 2010 Zmanda, Inc. All Rights Reserved.
#
# This program is free software; you can redistribute it and/or modify it
# under the terms of the GNU General Public License version 2 as published
# with this program; if not, write to the Free Software Foundation, Inc.,
# 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
#
-# Contact information: Zmanda Inc, 465 S Mathlida Ave, Suite 300
+# Contact information: Zmanda Inc, 465 S. Mathilda Ave., Suite 300
# Sunnyvale, CA 94086, USA, or: http://www.zmanda.com
-use Test::More tests => 35;
+use Test::More tests => 54;
use File::Path;
+use Data::Dumper;
use strict;
+use warnings;
use lib "@amperldir@";
use Installcheck::Config;
use Amanda::Paths;
-use Amanda::Device;
+use Amanda::Device qw( :constants );;
use Amanda::Debug;
use Amanda::MainLoop;
use Amanda::Config qw( :init :getconf config_dir_relative );
use Amanda::Changer;
+use Amanda::Tapelist;
# set up debugging so debug output doesn't interfere with test results
Amanda::Debug::dbopen("installcheck");
+Installcheck::log_test_output();
# and disable Debug's die() and warn() overrides
Amanda::Debug::disable_die_override();
sub new {
my $class = shift;
- my ($cc, $tpchanger) = @_;
+ my ($config, $tpchanger) = @_;
my $self = {
+ config => $config,
curslot => 0,
slots => [ 'TAPE-00', 'TAPE-01', 'TAPE-02', 'TAPE-03' ],
reserved_slots => [],
}
$cb->(undef, Amanda::Changer::test::Reservation->new($self, $slot, $label));
- } elsif (exists $params{'slot'}) {
+ } elsif (exists $params{'slot'} or exists $params{'relative_slot'}) {
my $slot = $params{'slot'};
- $slot = $self->{'curslot'}
- if ($slot eq "current");
+ if (exists $params{'relative_slot'}) {
+ if ($params{'relative_slot'} eq "current") {
+ $slot = $self->{'curslot'};
+ } elsif ($params{'relative_slot'} eq "next") {
+ $slot = ($self->{'curslot'} + 1) % (scalar @{$self->{'slots'}});
+ } else {
+ die "invalid relative_slot";
+ }
+ }
if (grep { $_ == $slot } @{$self->{'reserved_slots'}}) {
$cb->("Slot $slot is already in use", undef);
}
}
+sub info_key {
+ my $self = shift;
+ my ($key, %params) = @_;
+ my %results;
+
+ if ($key eq 'num_slots') {
+ $results{$key} = 13;
+ } elsif ($key eq 'mkerror1') {
+ return $self->make_error("failed", $params{'info_cb'},
+ reason => "unknown",
+ message => "err1");
+ } elsif ($key eq 'mkerror2') {
+ return $self->make_error("failed", $params{'info_cb'},
+ reason => "unknown",
+ message => "err2");
+ }
+
+ $params{'info_cb'}->(undef, %results) if $params{'info_cb'};
+}
+
sub reset {
my $self = shift;
my %params = @_;
$self->{'curslot'} = 0;
- if (exists $params{'finished_cb'}) {
- Amanda::MainLoop::call_later($params{'finished_cb'}, undef);
- }
+ $params{'finished_cb'}->(undef) if $params{'finished_cb'};
}
sub clean {
$self->{'clean'} = 1;
- if (exists $params{'finished_cb'}) {
- Amanda::MainLoop::call_later($params{'finished_cb'}, undef);
- }
+ $params{'finished_cb'}->(undef) if $params{'finished_cb'};
}
+sub inventory {
+ my $self = shift;
+ my %params = @_;
+
+ Amanda::MainLoop::call_later($params{'inventory_cb'},
+ undef, [ {
+ slot => 1,
+ empty => 0,
+ label => 'TAPE-99',
+ barcode => '09385A',
+ reserved => 0,
+ import_export => 0,
+ loaded_in => undef,
+ }]);
+}
package Amanda::Changer::test::Reservation;
use vars qw( @ISA );
$self->{'slot'} = $slot;
$self->{'label'} = $label;
- $self->{'device_name'} = "test:slot-$slot";
+ $self->{'device'} = Amanda::Device->new("null:slot-$slot");
$self->{'this_slot'} = $slot;
- $self->{'next_slot'} = ($slot + 1) % (scalar @{$chg->{'slots'}});
return $self;
}
-sub release {
+sub do_release {
my $self = shift;
my %params = @_;
my $slot = $self->{'slot'};
$chg->{'reserved_slots'} = [ grep { $_ != $slot } @{$chg->{'reserved_slots'}} ];
- if (exists $params{'finished_cb'}) {
- Amanda::MainLoop::call_later($params{'finished_cb'}, undef);
- }
+ $params{'finished_cb'}->(undef) if $params{'finished_cb'};
}
sub set_label {
$self->{'chg'}->{'slots'}->[$self->{'slot'}] = $params{'label'};
$self->{'label'} = $params{'label'};
- if (exists $params{'finished_cb'}) {
- Amanda::MainLoop::call_later($params{'finished_cb'}, undef);
- }
+ $params{'finished_cb'}->(undef) if $params{'finished_cb'};
}
# --------
$testconf = Installcheck::Config->new();
$testconf->add_changer("mychanger", [
'tpchanger' => '"chg-test:/foo"',
+ 'property' => '"testprop" "testval"',
]);
$testconf->write();
die(join "\n", @errors);
}
-# test loading by label
+# check out the relevant changer properties
+my $tlf = Amanda::Config::config_dir_relative(getconf($CNF_TAPELIST));
+my $tl = Amanda::Tapelist->new($tlf);
+my $chg = Amanda::Changer->new("mychanger", tapelist => $tl);
+is($chg->{'config'}->get_property("testprop"), "testval",
+ "changer properties are correctly represented");
+is($chg->have_inventory(), 1, "changer have inventory");
+is($chg->make_new_tape_label(), undef, "no make_new_tape_label");
+is($chg->make_new_meta_label(), undef, "no make_new_meta_label");
+
+$chg = Amanda::Changer->new("mychanger", tapelist => $tl,
+ labelstr => "TESTCONF-[0-9][0-9][0-9]-[a-z][a-z][a-z]-[0-9][0-9][0-9]",
+ autolabel => { template => '$c-$m-$b-%%%',
+ other_config => 1,
+ non_amanda => 1,
+ volume_error => 0,
+ empty => 1 },
+ meta_autolabel => "%%%");
+my $meta = $chg->make_new_meta_label();
+is($meta, "001", "meta 001");
+my $label = $chg->make_new_tape_label(meta => $meta, barcode => 'aaa');
+is($label, 'TESTCONF-001-aaa-001', "label TESTCONF-001-aaa-001");
+
+is($chg->volume_is_labelable($DEVICE_STATUS_VOLUME_UNLABELED, $Amanda::Header::F_EMPTY),
+ 1, "empty volume is labelable");
+is($chg->volume_is_labelable($DEVICE_STATUS_VOLUME_ERROR, undef),
+ 0, "empty volume is labelable");
-my $chg = Amanda::Changer->new("mychanger");
+# test loading by label
{
- my @labels = ( 'TAPE-02', 'TAPE-00', 'TAPE-03' );
- my @reservations = ();
- my $getres;
+ my @labels;
+ my @reservations;
+ my ($getres, $rq_reserved, $relres);
+
+ $getres = make_cb('getres' => sub {
+ if (!@labels) {
+ return $rq_reserved->();
+ }
- $getres = sub {
my $label = pop @labels;
$chg->load(label => $label,
set_current => ($label eq "TAPE-02"),
res_cb => sub {
- my ($err, $reservation) = @_;
+ my ($err, $res) = @_;
ok(!$err, "no error loading $label")
or diag($err);
# keep this reservation
- if ($reservation) {
- push @reservations, $reservation;
- }
+ push @reservations, $res if $res;
# and start on the next
- if (@labels) {
- $getres->();
- return;
- } else {
- # try to load an already-reserved volume
- $chg->load(label => 'TAPE-00',
- res_cb => sub {
- my ($err, $reservation) = @_;
- ok($err, "error when requesting already-reserved volume");
- Amanda::MainLoop::quit();
- });
- }
+ $getres->();
});
- };
+ });
+
+ $rq_reserved = make_cb(rq_reserved => sub {
+ # try to load an already-reserved volume
+ $chg->load(label => 'TAPE-00',
+ res_cb => sub {
+ my ($err, $res) = @_;
+ ok($err, "error when requesting already-reserved volume");
+ push @reservations, $res if $res;
+
+ $relres->();
+ });
+ });
+
+ $relres = make_cb('relres' => sub {
+ if (!@reservations) {
+ return Amanda::MainLoop::quit();
+ }
+
+ my $res = pop @reservations;
+ $res->release(finished_cb => sub {
+ my ($err) = @_;
+ die $err if $err;
+
+ $relres->();
+ });
+ });
# start the loop
- Amanda::MainLoop::call_later($getres);
+ @labels = ( 'TAPE-02', 'TAPE-00', 'TAPE-03' );
+ $getres->();
+ Amanda::MainLoop::run();
+
+ $relres->();
Amanda::MainLoop::run();
- # ditch the reservations and do it all again
- @reservations = ();
@labels = ( 'TAPE-00', 'TAPE-01' );
- is_deeply($chg->{'reserved_slots'}, [],
- "reservations are released when the Reservation object goes out of scope");
- Amanda::MainLoop::call_later($getres);
+ $getres->();
Amanda::MainLoop::run();
# explicitly release the reservations (without using the callback)
# test loading by slot
{
- my ($start, $first_cb, $second_cb);
+ my ($start, $first_cb, $released, $second_cb, $quit);
+ my $slot;
# reserves the current slot
- $start = sub {
- $chg->load(res_cb => $first_cb, slot => "current");
- };
+ $start = make_cb('start' => sub {
+ $chg->load(res_cb => $first_cb, relative_slot => "current");
+ });
# gets a reservation for the "current" slot
- $first_cb = sub {
+ $first_cb = make_cb('first_cb' => sub {
my ($err, $res) = @_;
die $err if $err;
is($res->{'this_slot'}, 2,
"'current' slot loads slot 2");
- is($res->{'device_name'}, "test:slot-2",
+ is($res->{'device'}->device_name, "null:slot-2",
"..device is correct");
- is($res->{'next_slot'}, 3,
- "..and the next slot is slot 3");
- $chg->load(res_cb => $second_cb, slot => $res->{'next_slot'}, set_current => 1);
- };
+
+ $slot = $res->{'this_slot'};
+ $res->release(finished_cb => $released);
+ });
+
+ $released = make_cb(released => sub {
+ my ($err) = @_;
+
+ $chg->load(res_cb => $second_cb, relative_slot => 'next',
+ slot => $slot, set_current => 1);
+ });
# gets a reservation for the "next" slot
- $second_cb = sub {
+ $second_cb = make_cb('second_cb' => sub {
my ($err, $res) = @_;
die $err if $err;
"next slot loads slot 3");
is($chg->{'curslot'}, 3,
"..which is also now the current slot");
- is($res->{'next_slot'}, 0,
- "..and the next slot is slot 0");
+
+ $res->release(finished_cb => $quit);
+ });
+
+ $quit = make_cb(quit => sub {
+ my ($err) = @_;
+ die $err if $err;
Amanda::MainLoop::quit();
- };
+ });
- Amanda::MainLoop::call_later($start);
+ $start->();
Amanda::MainLoop::run();
}
# test set_label
{
- my ($start, $load1_cb, $set_cb, $load2_cb, $load3_cb);
+ my ($start, $load1_cb, $set_cb, $released, $load2_cb, $released2, $load3_cb);
+ my $res;
# load TAPE-00
- $start = sub {
+ $start = make_cb('start' => sub {
$chg->load(res_cb => $load1_cb, label => "TAPE-00");
- };
+ });
# rename it to TAPE-99
- $load1_cb = sub {
- my ($err, $res) = @_;
+ $load1_cb = make_cb('load1_cb' => sub {
+ (my $err, $res) = @_;
die $err if $err;
pass("loaded TAPE-00");
$res->set_label(label => "TAPE-99", finished_cb => $set_cb);
- $res->release();
- };
+ });
+
+ $set_cb = make_cb('set_cb' => sub {
+ my ($err) = @_;
+
+ $res->release(finished_cb => $released);
+ });
# try to load TAPE-00
- $set_cb = sub {
+ $released = make_cb('released' => sub {
my ($err) = @_;
die $err if $err;
pass("relabeled TAPE-00 to TAPE-99");
$chg->load(res_cb => $load2_cb, label => "TAPE-00");
- };
+ });
# try to load TAPE-99
- $load2_cb = sub {
- my ($err, $res) = @_;
-
+ $load2_cb = make_cb('load2_cb' => sub {
+ (my $err, $res) = @_;
ok($err, "loading TAPE-00 is now an error");
+
$chg->load(res_cb => $load3_cb, label => "TAPE-99");
- };
+ });
# check result
- $load3_cb = sub {
- my ($err, $res) = @_;
+ $load3_cb = make_cb('load3_cb' => sub {
+ (my $err, $res) = @_;
die $err if $err;
pass("but loading TAPE-99 is ok");
+ $res->release(finished_cb => $released2);
+ });
+
+ $released2 = make_cb(released2 => sub {
+ my ($err) = @_;
+ die $err if $err;
+
Amanda::MainLoop::quit();
- };
+ });
- Amanda::MainLoop::call_later($start);
+ $start->();
Amanda::MainLoop::run();
}
-# test reset and clean
-{
- my ($do_reset, $do_clean);
+# test reset and clean and inventory
+sub test_simple {
+ my ($finished_cb) = @_;
+
+ my $steps = define_steps
+ cb_ref => \$finished_cb;
- $do_reset = sub {
+ step do_reset => sub {
$chg->reset(finished_cb => sub {
is($chg->{'curslot'}, 0,
"reset() resets to slot 0");
- $do_clean->();
+ $steps->{'do_clean'}->();
});
};
- $do_clean = sub {
+ step do_clean => sub {
$chg->clean(finished_cb => sub {
ok($chg->{'clean'}, "clean 'cleaned' the changer");
- Amanda::MainLoop::quit();
+ $steps->{'do_inventory'}->();
+ });
+ };
+
+ step do_inventory => sub {
+ $chg->inventory(inventory_cb => sub {
+ is_deeply($_[1], [ {
+ slot => 1,
+ empty => 0,
+ label => 'TAPE-99',
+ barcode => '09385A',
+ reserved => 0,
+ import_export => 0,
+ loaded_in => undef,
+ }], "inventory returns an inventory");
+ $finished_cb->();
});
};
+}
+test_simple(\&Amanda::MainLoop::quit);
+Amanda::MainLoop::run();
- Amanda::MainLoop::call_later($do_reset);
+# test info
+{
+ my ($do_info, $check_info, $do_info_err, $check_info_err);
+
+ $do_info = make_cb('do_info' => sub {
+ $chg->info(info_cb => $check_info,
+ info => [ 'num_slots' ]);
+ });
+
+ $check_info = make_cb('check_info' => sub {
+ my ($err, %results) = @_;
+ die($err) if $err;
+ is_deeply(\%results, { 'num_slots' => 13 },
+ "info() works");
+ $do_info_err->();
+ });
+
+ $do_info_err = make_cb('do_info_err' => sub {
+ $chg->info(info_cb => $check_info_err,
+ info => [ 'mkerror1', 'mkerror2' ]);
+ });
+
+ $check_info_err = make_cb('check_info_err' => sub {
+ my ($err, %results) = @_;
+ is($err,
+ "While getting info key 'mkerror1': err1; While getting info key 'mkerror2': err2",
+ "info errors are handled correctly");
+ is($err->{'type'}, 'failed', "error has type 'failed'");
+ ok($err->failed, "\$err->failed is true");
+ ok(!$err->fatal, "\$err->fatal is false");
+ is($err->{'reason'}, 'unknown', "\$err->{'reason'} is 'unknown'");
+ ok($err->unknown, "\$err->unknown is true");
+ ok(!$err->notimpl, "\$err->notimpl is false");
+ Amanda::MainLoop::quit();
+ });
+
+ $do_info->();
Amanda::MainLoop::run();
}
+$chg->quit();
# Test the various permutations of configuration setup, with a patched
# _new_from_uri so we can monitor the result
sub my_new_from_uri {
my ($uri, $cc, $name) = @_;
+ return $uri if (ref $uri and $uri->isa("Amanda::Changer::Error"));
return [ $uri, $cc? "cc" : undef ];
}
*saved_new_from_uri = *Amanda::Changer::_new_from_uri;
*Amanda::Changer::_new_from_uri = *my_new_from_uri;
sub loadconfig {
- my ($global_tapedev, $global_tpchanger, $defn_tpchanger) = @_;
+ my ($global_tapedev, $global_tpchanger, $defn_tpchanger, $custom_defn) = @_;
$testconf = Installcheck::Config->new();
]);
}
+ if (defined($custom_defn)) {
+ $testconf->add_changer("customchanger", $custom_defn);
+ $testconf->add_param('tpchanger', '"customchanger"');
+ }
+
$testconf->write();
my $cfg_result = config_init($CONFIG_INIT_EXPLICIT_NAME, 'TESTCONF');
}
sub assert_invalid {
- my ($global_tapedev, $global_tpchanger, $defn_tpchanger, $name, $msg) = @_;
- loadconfig($global_tapedev, $global_tpchanger, $defn_tpchanger);
- eval { Amanda::Changer->new($name); };
- ok($@, $msg);
+ my ($global_tapedev, $global_tpchanger, $defn_tpchanger, $custom_defn,
+ $name, $regexp, $msg) = @_;
+ loadconfig($global_tapedev, $global_tpchanger, $defn_tpchanger, $custom_defn);
+ my $err = Amanda::Changer->new($name);
+ if ($err->isa("Amanda::Changer::Error")) {
+ like($err->{'message'}, $regexp, $msg);
+ } else {
+ diag("Amanda::Changer->new did not return an Error object:");
+ diag("".Dumper($err));
+ fail($msg);
+ }
}
-assert_invalid(undef, undef, undef, undef,
+assert_invalid(undef, undef, undef, undef, undef,
+ qr/You must specify one of 'tapedev' or 'tpchanger'/,
"supplying a nothing is invalid");
-loadconfig(undef, "file:/foo", undef);
+loadconfig(undef, "file:/foo", undef, undef);
is_deeply( Amanda::Changer->new(), [ "chg-single:file:/foo", undef ],
"default changer with global tpchanger naming a device");
-loadconfig(undef, "chg-disk:/foo", undef);
+loadconfig(undef, "chg-disk:/foo", undef, undef);
is_deeply( Amanda::Changer->new(), [ "chg-disk:/foo", undef ],
"default changer with global tpchanger naming a changer");
-loadconfig(undef, "mychanger", "chg-disk:/bar");
+loadconfig(undef, "mychanger", "chg-disk:/bar", undef);
is_deeply( Amanda::Changer->new(), [ "chg-disk:/bar", "cc" ],
"default changer with global tpchanger naming a defined changer with a uri");
-loadconfig(undef, "mychanger", "chg-zd-mtx");
+loadconfig(undef, "mychanger", "chg-zd-mtx", undef);
is_deeply( Amanda::Changer->new(), [ "chg-compat:chg-zd-mtx", "cc" ],
"default changer with global tpchanger naming a defined changer with a compat script");
-loadconfig(undef, "chg-zd-mtx", undef);
+loadconfig(undef, "chg-zd-mtx", undef, undef);
is_deeply( Amanda::Changer->new(), [ "chg-compat:chg-zd-mtx", undef ],
"default changer with global tpchanger naming a compat script");
-loadconfig("tape:/dev/foo", undef, undef);
+loadconfig("tape:/dev/foo", undef, undef, undef);
is_deeply( Amanda::Changer->new(), [ "chg-single:tape:/dev/foo", undef ],
"default changer with global tapedev naming a device and no tpchanger");
-assert_invalid("tape:/dev/foo", "tape:/dev/foo", undef, undef,
+assert_invalid("tape:/dev/foo", "tape:/dev/foo", undef, undef, undef,
+ qr/Cannot specify both 'tapedev' and 'tpchanger'/,
"supplying a device for both tpchanger and tapedev is invalid");
-assert_invalid("tape:/dev/foo", "chg-disk:/foo", undef, undef,
+assert_invalid("tape:/dev/foo", "chg-disk:/foo", undef, undef, undef,
+ qr/Cannot specify both 'tapedev' and 'tpchanger'/,
"supplying a device for tapedev and a changer for tpchanger is invalid");
-loadconfig("tape:/dev/foo", 'chg-zd-mtx', undef);
+loadconfig("tape:/dev/foo", 'chg-zd-mtx', undef, undef);
is_deeply( Amanda::Changer->new(), [ "chg-compat:chg-zd-mtx", undef ],
"default changer with global tapedev naming a device and a global tpchanger naming a compat script");
-assert_invalid("chg-disk:/foo", "tape:/dev/foo", undef, undef,
+assert_invalid("chg-disk:/foo", "tape:/dev/foo", undef, undef, undef,
+ qr/Cannot specify both 'tapedev' and 'tpchanger'/,
"supplying a changer for tapedev and a device for tpchanger is invalid");
-loadconfig("chg-disk:/foo", undef, undef);
+loadconfig("chg-disk:/foo", undef, undef, undef);
is_deeply( Amanda::Changer->new(), [ "chg-disk:/foo", undef ],
"default changer with global tapedev naming a device");
-loadconfig("mychanger", undef, "chg-disk:/bar");
+loadconfig("mychanger", undef, "chg-disk:/bar", undef);
is_deeply( Amanda::Changer->new(), [ "chg-disk:/bar", "cc" ],
"default changer with global tapedev naming a defined changer with a uri");
-loadconfig("mychanger", undef, "chg-zd-mtx");
+loadconfig("mychanger", undef, "chg-zd-mtx", undef);
is_deeply( Amanda::Changer->new(), [ "chg-compat:chg-zd-mtx", "cc" ],
"default changer with global tapedev naming a defined changer with a compat script");
-loadconfig(undef, undef, "chg-disk:/foo");
+loadconfig(undef, undef, "chg-disk:/foo", undef);
is_deeply( Amanda::Changer->new("mychanger"), [ "chg-disk:/foo", "cc" ],
"named changer loads the proper definition");
+loadconfig(undef, undef, undef, [
+ tapedev => '"chg-disk:/foo"',
+]);
+is_deeply( Amanda::Changer->new(), [ "chg-disk:/foo", "cc" ],
+ "defined changer with tapedev loads the proper definition");
+
+loadconfig(undef, undef, undef, [
+ tpchanger => '"chg-disk:/bar"',
+]);
+is_deeply( Amanda::Changer->new(), [ "chg-disk:/bar", "cc" ],
+ "defined changer with tpchanger loads the proper definition");
+
+assert_invalid(undef, undef, undef, [
+ tpchanger => '"chg-disk:/bar"',
+ tapedev => '"file:/bar"',
+ ], undef,
+ qr/Cannot specify both 'tapedev' and 'tpchanger'/,
+ "supplying both a new tpchanger and tapedev in a definition is invalid");
+
+assert_invalid(undef, undef, undef, [
+ property => '"this" "will not work"',
+ ], undef,
+ qr/You must specify one of 'tapedev' or 'tpchanger'/,
+ "supplying neither a tpchanger nor tapedev in a definition is invalid");
+
*Amanda::Changer::_new_from_uri = *saved_new_from_uri;
+
+# test with_locked_state *within* a process
+
+sub test_locked_state {
+ my ($finished_cb) = @_;
+ my $chg;
+ my $stfile = "$Installcheck::TMP/test-statefile";
+ my $num_outstanding = 0;
+
+ my $steps = define_steps
+ cb_ref => \$finished_cb,
+ finalize => sub { $chg->quit() if defined $chg };
+
+ step start => sub {
+ $chg = Amanda::Changer->new("chg-null:");
+
+ for my $num (qw( one two three )) {
+ ++$num_outstanding;
+ $chg->with_locked_state($stfile, $steps->{'maybe_done'}, sub {
+ my ($state, $maybe_done) = @_;
+
+ $state->{$num} = $num;
+ $state->{'count'}++;
+
+ Amanda::MainLoop::call_after(50, $maybe_done, undef, $state);
+ });
+ }
+ };
+
+ step maybe_done => sub {
+ my ($err, $state) = @_;
+ die $err if $err;
+
+ return if (--$num_outstanding);
+
+ is_deeply($state, {
+ one => "one",
+ two => "two",
+ three => "three",
+ count => 3,
+ }, "state is maintained correctly (within a process)");
+
+ unlink($stfile) if -f $stfile;
+
+ $finished_cb->();
+ };
+}
+test_locked_state(\&Amanda::MainLoop::quit);
+Amanda::MainLoop::run();