--- deliantra/server/lib/cf.pm 2007/04/16 11:09:32 1.241 +++ deliantra/server/lib/cf.pm 2007/06/11 22:16:53 1.279 @@ -5,14 +5,16 @@ use Symbol; use List::Util; +use Socket; use Storable; use Event; use Opcode; use Safe; use Safe::Hole; -use Coro 3.52 (); +use Coro 3.61 (); use Coro::State; +use Coro::Handle; use Coro::Event; use Coro::Timer; use Coro::Signal; @@ -54,9 +56,20 @@ our $RELOAD; # number of reloads so far our @EVENT; -our $LIBDIR = datadir . "/ext"; -our $TICK = MAX_TIME * 1e-6; +our $CONFDIR = confdir; +our $DATADIR = datadir; +our $LIBDIR = "$DATADIR/ext"; +our $PODDIR = "$DATADIR/pod"; +our $MAPDIR = "$DATADIR/" . mapdir; +our $LOCALDIR = localdir; +our $TMPDIR = "$LOCALDIR/" . tmpdir; +our $UNIQUEDIR = "$LOCALDIR/" . uniquedir; +our $PLAYERDIR = "$LOCALDIR/" . playerdir; +our $RANDOMDIR = "$LOCALDIR/random"; +our $BDBDIR = "$LOCALDIR/db"; + +our $TICK = MAX_TIME * 1e-6; # this is a CONSTANT(!) our $TICK_WATCHER; our $AIO_POLL_WATCHER; our $NEXT_RUNTIME_WRITE; # when should the runtime file be written @@ -75,28 +88,26 @@ our %PLAYER; # all users our %MAP; # all maps our $LINK_MAP; # the special {link} map, which is always available -our $RANDOM_MAPS = cf::localdir . "/random"; -our $BDB_ENV_DIR = cf::localdir . "/db"; # used to convert map paths into valid unix filenames by replacing / by ∕ our $PATH_SEP = "∕"; # U+2215, chosen purely for visual reasons +our $LOAD; # a number between 0 (idle) and 1 (too many objects) +our $LOADAVG; # same thing, but with alpha-smoothing +our $tick_start; # for load detecting purposes + binmode STDOUT; binmode STDERR; # read virtual server time, if available -unless ($RUNTIME || !-e cf::localdir . "/runtime") { - open my $fh, "<", cf::localdir . "/runtime" +unless ($RUNTIME || !-e "$LOCALDIR/runtime") { + open my $fh, "<", "$LOCALDIR/runtime" or die "unable to read runtime file: $!"; $RUNTIME = <$fh> + 0.; } -mkdir cf::localdir; -mkdir cf::localdir . "/" . cf::playerdir; -mkdir cf::localdir . "/" . cf::tmpdir; -mkdir cf::localdir . "/" . cf::uniquedir; -mkdir $RANDOM_MAPS; -mkdir $BDB_ENV_DIR; +mkdir $_ + for $LOCALDIR, $TMPDIR, $UNIQUEDIR, $PLAYERDIR, $RANDOMDIR, $BDBDIR; our $EMERGENCY_POSITION; @@ -117,10 +128,14 @@ The time this server has run, starts at 0 and is increased by $cf::TICK on every server tick. -=item $cf::LIBDIR - -The perl library directory, where extensions and cf-specific modules can -be found. It will be added to C<@INC> automatically. +=item $cf::CONFDIR $cf::DATADIR $cf::LIBDIR $cf::PODDIR +$cf::MAPDIR $cf::LOCALDIR $cf::TMPDIR $cf::UNIQUEDIR +$cf::PLAYERDIR $cf::RANDOMDIR $cf::BDBDIR + +Various directories - "/etc", read-only install directory, perl-library +directory, pod-directory, read-only maps directory, "/var", "/var/tmp", +unique-items directory, player file directory, random maps directory and +database environment. =item $cf::NOW @@ -130,6 +145,16 @@ The interval between server ticks, in seconds. +=item $cf::LOADAVG + +The current CPU load on the server (alpha-smoothed), as a value between 0 +(none) and 1 (overloaded), indicating how much time is spent on processing +objects per tick. Healthy values are < 0.5. + +=item $cf::LOAD + +The raw value load value from the last tick. + =item %cf::CFG Configuration for the server, loaded from C, or @@ -148,11 +173,13 @@ BEGIN { *CORE::GLOBAL::warn = sub { my $msg = join "", @_; - utf8::encode $msg; $msg .= "\n" unless $msg =~ /\n$/; + $msg =~ s/([\x00-\x08\x0b-\x1f])/sprintf "\\x%02x", ord $1/ge; + + utf8::encode $msg; LOG llevError, $msg; }; } @@ -162,7 +189,8 @@ @safe::cf::player::ISA = @cf::player::ISA = 'cf::attachable'; @safe::cf::client::ISA = @cf::client::ISA = 'cf::attachable'; @safe::cf::map::ISA = @cf::map::ISA = 'cf::attachable'; -@safe::cf::object::player::ISA = @cf::object::player::ISA = 'cf::object'; +@safe::cf::arch::ISA = @cf::arch::ISA = 'cf::object'; +@safe::cf::object::player::ISA = @cf::object::player::ISA = 'cf::object'; # not really true (yet) # we bless all objects into (empty) derived classes to force a method lookup # within the Safe compartment. @@ -232,6 +260,10 @@ Lock names should begin with a unique identifier (for example, cf::map::find uses map_find and cf::map::load uses map_load). +=item $locked = cf::lock_active $string + +Return true if the lock is currently active, i.e. somebody has locked it. + =cut our %LOCK; @@ -260,6 +292,12 @@ } } +sub lock_active($) { + my ($key) = @_; + + ! ! $LOCK{$key} +} + sub freeze_mainloop { return unless $TICK_WATCHER->is_active; @@ -297,6 +335,8 @@ my ($job) = @_; if ($Coro::current == $Coro::main) { + my $time = Event::time; + # this is the main coro, too bad, we have to block # till the operation succeeds, freezing the server :/ @@ -317,6 +357,13 @@ Coro::cede or Event::one_event; } + $time = Event::time - $time; + + LOG llevError | logBacktrace, Carp::longmess "long sync job" + if $time > $TICK * 0.5 && $TICK_WATCHER->is_active; + + $tick_start += $time; # do not account sync jobs to server load + wantarray ? @res : $res[0] } else { # we are in another coroutine, how wonderful, everything just works @@ -346,9 +393,14 @@ } sub write_runtime { - my $guard = cf::lock_acquire "write_runtime"; + my $runtime = "$LOCALDIR/runtime"; + + # first touch the runtime file to show we are still running: + # the fsync below can take a very very long time. - my $runtime = cf::localdir . "/runtime"; + IO::AIO::aio_utime $runtime, undef, undef; + + my $guard = cf::lock_acquire "write_runtime"; my $fh = aio_open "$runtime~", O_WRONLY | O_CREAT, 0644 or return; @@ -364,12 +416,17 @@ aio_fsync $fh and return; + # touch it again to show we are up-to-date + aio_utime $fh, undef, undef; + close $fh or return; aio_rename "$runtime~", $runtime and return; + warn "runtime file written.\n"; + 1 } @@ -571,7 +628,7 @@ } elsif ($type eq "subtype") { defined $object_type or Carp::croak "subtype specified without type"; my $object_subtype = shift @arg; - $registry = $CB_TYPE[$object_type + $object_subtype * NUM_SUBTYPES] ||= []; + $registry = $CB_TYPE[$object_type + $object_subtype * NUM_TYPES] ||= []; } elsif ($type eq "package") { my $pkg = shift @arg; @@ -622,6 +679,7 @@ } else { _attach shift->_attach_registry, @_; } + _recalc_want; }; # all those should be optimised @@ -634,6 +692,7 @@ } else { Carp::croak "cannot, currently, detach class attachments"; } + _recalc_want; }; sub cf::attachable::attached { @@ -816,7 +875,7 @@ } warn sprintf "loading %s (%d)\n", - $filename, length $data, scalar @{$av || []};#d# + $filename, length $data, scalar @{$av || []}; return ($data, $av); } @@ -886,48 +945,77 @@ }, ); -sub load_extension { - my ($path) = @_; +sub load_extensions { + cf::sync_job { + my %todo; - $path =~ /([^\/\\]+)\.ext$/ or die "$path"; - my $base = $1; - my $pkg = $1; - $pkg =~ s/[^[:word:]]/_/g; - $pkg = "ext::$pkg"; + for my $path (<$LIBDIR/*.ext>) { + next unless -r $path; - warn "... loading '$path' into '$pkg'\n"; + $path =~ /([^\/\\]+)\.ext$/ or die "$path"; + my $base = $1; + my $pkg = $1; + $pkg =~ s/[^[:word:]]/_/g; + $pkg = "ext::$pkg"; - open my $fh, "<:utf8", $path - or die "$path: $!"; + open my $fh, "<:utf8", $path + or die "$path: $!"; + + my $source = do { local $/; <$fh> }; + + my %ext = ( + path => $path, + base => $base, + pkg => $pkg, + ); + + $ext{meta} = { map { (split /=/, $_, 2)[0, 1] } split /\s+/, $1 } + if $source =~ /\A#!.*?perl.*?#\s*(.*)$/m; - my $source = - "package $pkg; use strict; use utf8;\n" - . "#line 1 \"$path\"\n{\n" - . (do { local $/; <$fh> }) - . "\n};\n1"; - - unless (eval $source) { - my $msg = $@ ? "$path: $@\n" - : "extension disabled.\n"; - if ($source =~ /^#!.*perl.*#.*MANDATORY/m) { # ugly match - warn $@; - warn "mandatory extension failed to load, exiting.\n"; - exit 1; + $ext{source} = + "package $pkg; use strict; use utf8;\n" + . "#line 1 \"$path\"\n{\n" + . $source + . "\n};\n1"; + + $todo{$base} = \%ext; } - die $@; - } - push @EXTS, $pkg; -} + my %done; + while (%todo) { + my $progress; -sub load_extensions { - for my $ext (<$LIBDIR/*.ext>) { - next unless -r $ext; - eval { - load_extension $ext; - 1 - } or warn "$ext not loaded: $@"; - } + while (my ($k, $v) = each %todo) { + for (split /,\s*/, $v->{meta}{depends}) { + goto skip + unless exists $done{$_}; + } + + warn "... loading '$k' into '$v->{pkg}'\n"; + + unless (eval $v->{source}) { + my $msg = $@ ? "$v->{path}: $@\n" + : "$v->{base}: extension inactive.\n"; + + if (exists $v->{meta}{mandatory}) { + warn $msg; + warn "mandatory extension failed to load, exiting.\n"; + exit 1; + } + + warn $msg; + } + + $done{$k} = delete $todo{$k}; + push @EXTS, $v->{pkg}; + $progress = 1; + } + + skip: + die "cannot load " . (join ", ", keys %todo) . ": unable to resolve dependencies\n" + unless $progress; + } + }; } ############################################################################# @@ -953,10 +1041,7 @@ =cut sub playerdir($) { - cf::localdir - . "/" - . cf::playerdir - . "/" + "$PLAYERDIR/" . (ref $_[0] ? $_[0]->ob->name : $_[0]) } @@ -1086,7 +1171,7 @@ =cut sub list_logins { - my $dirs = aio_readdir cf::localdir . "/" . cf::playerdir + my $dirs = aio_readdir $PLAYERDIR or return []; my @logins; @@ -1251,8 +1336,6 @@ # we have to keep some variables in memory intact local $self->{path}; local $self->{load_path}; - local $self->{deny_save}; - local $self->{deny_reset}; $self->SUPER::thawer_merge ($merge); } @@ -1334,7 +1417,7 @@ sub load_path { my ($self) = @_; - sprintf "%s/%s/%s.map", cf::datadir, cf::mapdir, $self->{path} + "$MAPDIR/$self->{path}.map" } # the temporary/swap location @@ -1342,7 +1425,7 @@ my ($self) = @_; (my $path = $_[0]{path}) =~ s/\//$PATH_SEP/g; - sprintf "%s/%s/%s.map", cf::localdir, cf::tmpdir, $path + "$TMPDIR/$path.map" } # the unique path, undef == no special unique path @@ -1350,7 +1433,7 @@ my ($self) = @_; (my $path = $_[0]{path}) =~ s/\//$PATH_SEP/g; - sprintf "%s/%s/%s", cf::localdir, cf::uniquedir, $path + "$UNIQUEDIR/$path" } # and all this just because we cannot iterate over @@ -1362,6 +1445,14 @@ for grep $_->outdoor, values %cf::MAP; } +sub decay_objects { + my ($self) = @_; + + return if $self->{deny_reset}; + + $self->do_decay_objects; +} + sub unlink_save { my ($self) = @_; @@ -1427,6 +1518,9 @@ $self->prepare_orig; } + $self->{deny_reset} = 1 + if $self->no_reset; + $self->default_region (cf::region::find_by_path $self->{path}) unless $self->default_region; @@ -1451,7 +1545,7 @@ $map->load_header or return; - if ($map->should_reset && 0) {#d#TODO# disabled, crashy (locking issue?) + if ($map->should_reset) {#d#TODO# disabled, crashy (locking issue?) # doing this can freeze the server in a sync job, obviously #$cf::WAIT_FOR_TICK->wait; $map->reset; @@ -1472,53 +1566,54 @@ local $self->{deny_reset} = 1; # loading can take a long time my $path = $self->{path}; - my $guard = cf::lock_acquire "map_load:$path"; - return if $self->in_memory != cf::MAP_SWAPPED; + { + my $guard = cf::lock_acquire "map_load:$path"; - $self->in_memory (cf::MAP_LOADING); + return if $self->in_memory != cf::MAP_SWAPPED; - $self->alloc; + $self->in_memory (cf::MAP_LOADING); - $self->pre_load; - Coro::cede; + $self->alloc; - $self->_load_objects ($self->{load_path}, 1) - or return; + $self->pre_load; + Coro::cede; - $self->set_object_flag (cf::FLAG_OBJ_ORIGINAL, 1) - if delete $self->{load_original}; + $self->_load_objects ($self->{load_path}, 1) + or return; - if (my $uniq = $self->uniq_path) { - utf8::encode $uniq; - if (aio_open $uniq, O_RDONLY, 0) { - $self->clear_unique_items; - $self->_load_objects ($uniq, 0); - } - } + $self->set_object_flag (cf::FLAG_OBJ_ORIGINAL, 1) + if delete $self->{load_original}; - Coro::cede; - # now do the right thing for maps - $self->link_multipart_objects; - Coro::cede; + if (my $uniq = $self->uniq_path) { + utf8::encode $uniq; + if (aio_open $uniq, O_RDONLY, 0) { + $self->clear_unique_items; + $self->_load_objects ($uniq, 0); + } + } - unless ($self->{deny_activate}) { - $self->decay_objects; - $self->fix_auto_apply; - $self->update_buttons; Coro::cede; - $self->set_darkness_map; + # now do the right thing for maps + $self->link_multipart_objects; $self->difficulty ($self->estimate_difficulty) unless $self->difficulty; Coro::cede; - $self->activate; - Coro::cede; + + unless ($self->{deny_activate}) { + $self->decay_objects; + $self->fix_auto_apply; + $self->update_buttons; + Coro::cede; + $self->set_darkness_map; + Coro::cede; + $self->activate; + } + + $self->in_memory (cf::MAP_IN_MEMORY); } $self->post_load; - Coro::cede; - - $self->in_memory (cf::MAP_IN_MEMORY); } sub customise_for { @@ -1527,6 +1622,9 @@ return find "~" . $ob->name . "/" . $self->{path} if $self->per_player; +# return find "?party/" . $ob->name . "/" . $self->{path} +# if $self->per_party; + $self } @@ -1646,7 +1744,6 @@ my ($self) = @_; # TODO: safety, remove and allow resettable per-player maps - return 1e99 if $self->isa ("ext::map_per_player");#d# return 1e99 if $self->{deny_reset}; my $time = $self->fixed_resettime ? $self->{instantiate_time} : $self->last_access; @@ -1667,9 +1764,8 @@ my $lock = cf::lock_acquire "map_data:$self->{path}"; return if $self->players; - return if $self->isa ("ext::map_per_player");#d# - warn "resetting map ", $self->path;#d# + warn "resetting map ", $self->path; $self->in_memory (cf::MAP_SWAPPED); @@ -1711,28 +1807,58 @@ $self->reset; # polite request, might not happen } -=item cf::map::unique_maps +=item $maps = cf::map::tmp_maps -Returns an arrayref of paths of all shared maps that have -instantiated unique items. May block. +Returns an arrayref with all map paths of currently instantiated and saved +maps. May block. =cut -sub unique_maps() { - my $files = aio_readdir cf::localdir . "/" . cf::uniquedir - or return; +sub tmp_maps() { + [ + map { + utf8::decode $_; + /\.map$/ + ? normalise $_ + : () + } @{ aio_readdir $TMPDIR or [] } + ] +} - my @paths; +=item $maps = cf::map::random_maps - for (@$files) { - utf8::decode $_; - next if /\.pst$/; - next unless /^$PATH_SEP/o; +Returns an arrayref with all map paths of currently instantiated and saved +random maps. May block. - push @paths, cf::map::normalise $_; - } +=cut - \@paths +sub random_maps() { + [ + map { + utf8::decode $_; + /\.map$/ + ? normalise "?random/$_" + : () + } @{ aio_readdir $RANDOMDIR or [] } + ] +} + +=item cf::map::unique_maps + +Returns an arrayref of paths of all shared maps that have +instantiated unique items. May block. + +=cut + +sub unique_maps() { + [ + map { + utf8::decode $_; + /\.map$/ + ? normalise $_ + : () + } @{ aio_readdir $UNIQUEDIR or [] } + ] } package cf; @@ -1848,7 +1974,7 @@ return if UNIVERSAL::isa $self->map, "ext::map_link"; $self->{_link_pos} ||= [$self->map->{path}, $self->x, $self->y] - if $self->map; + if $self->map && $self->map->{path} ne "{link}"; $self->enter_map ($LINK_MAP || link_map, 10, 10); } @@ -1856,6 +1982,8 @@ sub cf::object::player::leave_link { my ($self, $map, $x, $y) = @_; + return unless $self->contr->active; + my $link_pos = delete $self->{_link_pos}; unless ($map) { @@ -1887,56 +2015,48 @@ $self->enter_map ($map, $x, $y); } -cf::player->attach ( - on_logout => sub { - my ($pl) = @_; - - # abort map switching before logout - if ($pl->ob->{_link_pos}) { - cf::sync_job { - $pl->ob->leave_link - }; - } - }, - on_login => sub { - my ($pl) = @_; - - # try to abort aborted map switching on player login :) - # should happen only on crashes - if ($pl->ob->{_link_pos}) { - $pl->ob->enter_link; - (async { - $pl->ob->reply (undef, - "There was an internal problem at your last logout, " - . "the server will try to bring you to your intended destination in a second.", - cf::NDI_RED); - # we need this sleep as the login has a concurrent enter_exit running - # and this sleep increases chances of the player not ending up in scorn - Coro::Timer::sleep 1; - $pl->ob->leave_link; - })->prio (2); - } - }, -); +=item $player_object->goto ($path, $x, $y[, $check->($map)]) -=item $player_object->goto ($path, $x, $y) +Moves the player to the given map-path and coordinates by first freezing +her, loading and preparing them map, calling the provided $check callback +that has to return the map if sucecssful, and then unfreezes the player on +the new (success) or old (failed) map position. =cut +our $GOTOGEN; + sub cf::object::player::goto { - my ($self, $path, $x, $y) = @_; + my ($self, $path, $x, $y, $check) = @_; + + # do generation counting so two concurrent goto's will be executed in-order + my $gen = $self->{_goto_generation} = ++$GOTOGEN; $self->enter_link; (async { my $map = eval { my $map = cf::map::find $path; - $map = $map->customise_for ($self) if $map; + + if ($map) { + $map = $map->customise_for ($self); + $map = $check->($map) if $check && $map; + } else { + $self->message ("The exit to '$path' is closed", cf::NDI_UNIQUE | cf::NDI_RED); + } + $map - } or - $self->message ("The exit to '$path' is closed", cf::NDI_UNIQUE | cf::NDI_RED); + }; - $self->leave_link ($map, $x, $y); + if ($@) { + $self->message ("Something went wrong within the server, please report this incident!", cf::NDI_UNIQUE | cf::NDI_RED); + LOG llevError | logBacktrace, Carp::longmess $@; + } + + if ($gen == $self->{_goto_generation}) { + delete $self->{_goto_generation}; + $self->leave_link ($map, $x, $y); + } })->prio (1); } @@ -1985,7 +2105,7 @@ my $data = cf::to_json $rmp; my $md5 = Digest::MD5::md5_hex $data; - my $meta = "$cf::RANDOM_MAPS/$md5.meta"; + my $meta = "$RANDOMDIR/$md5.meta"; if (my $fh = aio_open "$meta~", O_WRONLY | O_CREAT, 0666) { aio_write $fh, 0, (length $data), $data, 0; @@ -2264,8 +2384,8 @@ =head2 EXTENSION DATABASE SUPPORT Crossfire maintains a very simple database for extension use. It can -currently store anything that can be serialised using Storable, which -excludes objects. +currently store binary data only (use Compress::LZF::sfreeze_cr/sthaw to +convert to/from binary). The parameter C<$family> should best start with the name of the extension using it, it should be unique. @@ -2298,20 +2418,6 @@ }; cf::cleanup "db_open(db): $@" if $@; }; - - my $path = cf::localdir . "/database.pst"; - if (stat $path) { - cf::sync_job { - my $pst = Storable::retrieve $path; - - cf::db_put (board => data => $pst->{board}); - cf::db_put (guildrules => data => $pst->{guildrules}); - cf::db_put (rent => balance => $pst->{rent}{balance}); - BDB::db_env_txn_checkpoint $DB_ENV; - - unlink $path; - }; - } } } @@ -2322,20 +2428,134 @@ BDB::db_get $DB, undef, $key, my $data; $! ? () - : Compress::LZF::sthaw $data + : $data } } sub db_put($$$) { BDB::dbreq_pri 4; - BDB::db_put $DB, undef, "$_[0]/$_[1]", Compress::LZF::sfreeze_cr $_[2], 0, sub { }; + BDB::db_put $DB, undef, "$_[0]/$_[1]", $_[2], 0, sub { }; +} + +=item cf::cache $id => [$paths...], $processversion => $process + +Generic caching function that returns the value of the resource $id, +caching and regenerating as required. + +This function can block. + +=cut + +sub cache { + my ($id, $src, $processversion, $process) = @_; + + my $meta = + join "\x00", + $processversion, + map { + aio_stat $_ + and Carp::croak "$_: $!"; + + ($_, (stat _)[7,9]) + } @$src; + + my $dbmeta = db_get cache => "$id/meta"; + if ($dbmeta ne $meta) { + # changed, we may need to process + + my @data; + my $md5; + + for (0 .. $#$src) { + 0 <= aio_load $src->[$_], $data[$_] + or Carp::croak "$src->[$_]: $!"; + } + + # if processing is expensive, check + # checksum first + if (1) { + $md5 = + join "\x00", + $processversion, + map { + Coro::cede; + ($src->[$_], Digest::MD5::md5_hex $data[$_]) + } 0.. $#$src; + + + my $dbmd5 = db_get cache => "$id/md5"; + if ($dbmd5 eq $md5) { + db_put cache => "$id/meta", $meta; + + return db_get cache => "$id/data"; + } + } + + my $t1 = Time::HiRes::time; + my $data = $process->(\@data); + my $t2 = Time::HiRes::time; + + warn "cache: '$id' processed in ", $t2 - $t1, "s\n"; + + db_put cache => "$id/data", $data; + db_put cache => "$id/md5" , $md5; + db_put cache => "$id/meta", $meta; + + return $data; + } + + db_get cache => "$id/data" +} + +=item fork_call { }, $args + +Executes the given code block with the given arguments in a seperate +process, returning the results. Everything must be serialisable with +Coro::Storable. May, of course, block. Note that the executed sub may +never block itself or use any form of Event handling. + +=cut + +sub fork_call(&@) { + my ($cb, @args) = @_; + +# socketpair my $fh1, my $fh2, Socket::AF_UNIX, Socket::SOCK_STREAM, Socket::PF_UNSPEC +# or die "socketpair: $!"; + pipe my $fh1, my $fh2 + or die "pipe: $!"; + + if (my $pid = fork) { + close $fh2; + + my $res = (Coro::Handle::unblock $fh1)->readline (undef); + $res = Coro::Storable::thaw $res; + + waitpid $pid, 0; # should not block anymore, we expect the child to simply behave + + die $$res unless "ARRAY" eq ref $res; + + return wantarray ? @$res : $res->[-1]; + } else { + reset_signals; + local $SIG{__WARN__}; + local $SIG{__DIE__}; + eval { + close $fh1; + + my @res = eval { $cb->(@args) }; + syswrite $fh2, Coro::Storable::freeze +($@ ? \"$@" : \@res); + }; + + warn $@ if $@; + _exit 0; + } } ############################################################################# # the server's init and main functions -sub load_facedata { - my $path = sprintf "%s/facedata", cf::datadir; +sub load_facedata($) { + my ($path) = @_; warn "loading facedata from $path\n"; @@ -2386,34 +2606,44 @@ 1 } -our $archetypes_loaded; - -sub load_archetypes { - return 1 if $archetypes_loaded++; # current can only load them once - load_archetype_file sprintf "%s/archetypes", cf::datadir +sub reload_regions { + load_resource_file "$MAPDIR/regions" + or die "unable to load regions file\n"; } -sub load_treasures { - load_treasure_file sprintf "%s/treasures", cf::datadir +sub reload_facedata { + load_facedata "$DATADIR/facedata" + or die "unable to load facedata\n"; } -sub reload_resources { - load_resource_file sprintf "%s/%s/regions", cf::datadir, cf::mapdir - or die "unable to load regions file\n"; - load_facedata - or die "unable to load facedata\n"; - load_archetypes +sub reload_archetypes { + load_resource_file "$DATADIR/archetypes" or die "unable to load archetypes\n"; - load_treasures +} + +sub reload_treasures { + load_resource_file "$DATADIR/treasures" or die "unable to load treasurelists\n"; } +sub reload_resources { + warn "reloading resource files...\n"; + + reload_regions; + reload_facedata; + #reload_archetypes;#d# + reload_archetypes; + reload_treasures; + + warn "finished reloading resource files\n"; +} + sub init { reload_resources; } sub cfg_load { - open my $fh, "<:utf8", cf::confdir . "/config" + open my $fh, "<:utf8", "$CONFDIR/config" or return; local $/; @@ -2508,7 +2738,7 @@ if $make_core; } -sub reload() { +sub do_reload_perl() { # can/must only be called in main if ($Coro::current != $Coro::main) { warn "can only reload from main coroutine"; @@ -2593,6 +2823,7 @@ warn "unloading cf.pm \"a bit\""; delete $INC{"cf.pm"}; + delete $INC{"cf/pod.pm"}; # don't, removes xs symbols, too, # and global variables created in xs @@ -2617,9 +2848,6 @@ warn "reattaching attachments to players"; reattach $_ for values %PLAYER; - warn "loading reloadable resources"; - reload_resources; - warn "leaving sync_job"; 1 @@ -2634,24 +2862,27 @@ our $RELOAD_WATCHER; # used only during reload +sub reload_perl() { + # doing reload synchronously and two reloads happen back-to-back, + # coro crashes during coro_state_free->destroy here. + + $RELOAD_WATCHER ||= Event->timer ( + reentrant => 0, + after => 0, + data => WF_AUTOCANCEL, + cb => sub { + do_reload_perl; + undef $RELOAD_WATCHER; + }, + ); +} + register_command "reload" => sub { my ($who, $arg) = @_; if ($who->flag (FLAG_WIZ)) { $who->message ("reloading server."); - - # doing reload synchronously and two reloads happen back-to-back, - # coro crashes during coro_state_free->destroy here. - - $RELOAD_WATCHER ||= Event->timer ( - reentrant => 0, - after => 0, - data => WF_AUTOCANCEL, - cb => sub { - reload; - undef $RELOAD_WATCHER; - }, - ); + async { reload_perl }; } }; @@ -2680,6 +2911,8 @@ $signal->wait; } + my $min = 1e6;#d# + my $avg = 10; $TICK_WATCHER = Event->timer ( reentrant => 0, parked => 1, @@ -2693,9 +2926,33 @@ return; } - $NOW = Event::time; + $NOW = $tick_start = Event::time; cf::server_tick; # one server iteration + + 0 && sync_job {#d# + for(1..10) { + my $t = Event::time; + my $map = my $map = new_from_path cf::map "/tmp/x.map" + or die; + + $map->width (50); + $map->height (50); + $map->alloc; + $map->_load_objects ("/tmp/x.map", 1); + my $t = Event::time - $t; + + #next unless $t < 0.0013;#d# + if ($t < $min) { + $min = $t; + } + $avg = $avg * 0.99 + $t * 0.01; + } + warn "XXXXXXXXXXXXXXXXXX min $min avg $avg\n";#d# + exit 0; + # 2007-05-22 02:33:04.569 min 0.00112509727478027 avg 0.0012259249572477 + }; + $RUNTIME += $TICK; $NEXT_TICK += $TICK; @@ -2707,6 +2964,9 @@ }; } +# my $AFTER = Event::time; +# warn $AFTER - $NOW;#d# + if (my $sig = shift @WAIT_FOR_TICK_BEGIN) { $sig->send; } @@ -2714,14 +2974,20 @@ $sig->send; } -# my $AFTER = Event::time; -# warn $AFTER - $NOW;#d# + $NOW = Event::time; # if we are delayed by four ticks or more, skip them all - $NEXT_TICK = Event::time if Event::time >= $NEXT_TICK + $TICK * 4; + $NEXT_TICK = $NOW if $NOW >= $NEXT_TICK + $TICK * 4; $TICK_WATCHER->at ($NEXT_TICK); $TICK_WATCHER->start; + + $LOAD = ($NOW - $tick_start) / $TICK; + $LOADAVG = $LOADAVG * 0.75 + $LOAD * 0.25; + + _post_tick; + + }, ); @@ -2759,12 +3025,12 @@ eval { BDB::db_env_open $DB_ENV, - $BDB_ENV_DIR, + $BDBDIR, BDB::INIT_LOCK | BDB::INIT_LOG | BDB::INIT_MPOOL | BDB::INIT_TXN | BDB::RECOVER | BDB::REGISTER | BDB::USE_ENVIRON | BDB::CREATE, 0666; - cf::cleanup "db_env_open($BDB_ENV_DIR): $!" if $!; + cf::cleanup "db_env_open($BDBDIR): $!" if $!; $DB_ENV->set_flags (BDB::AUTO_COMMIT | BDB::REGION_INIT | BDB::TXN_NOSYNC, 1); $DB_ENV->set_lk_detect; @@ -2790,6 +3056,47 @@ ); } +my $_log_backtrace; + +sub _log_backtrace { + my ($msg, @addr) = @_; + + $msg =~ s/\n//; + + # limit the # of concurrent backtraces + if ($_log_backtrace < 2) { + ++$_log_backtrace; + async { + my @bt = fork_call { + @addr = map { sprintf "%x", $_ } @addr; + my $self = (-f "/proc/$$/exe") ? "/proc/$$/exe" : $^X; + open my $fh, "exec addr2line -C -f -i -e \Q$self\E @addr 2>&1 |" + or die "addr2line: $!"; + + my @funcs; + my @res = <$fh>; + chomp for @res; + while (@res) { + my ($func, $line) = splice @res, 0, 2, (); + push @funcs, "[$func] $line"; + } + + @funcs + }; + + LOG llevInfo, "[ABT] $msg\n"; + LOG llevInfo, "[ABT] $_\n" for @bt; + --$_log_backtrace; + }; + } else { + LOG llevInfo, "[ABT] $msg\n"; + LOG llevInfo, "[ABT] [suppressed]\n"; + } +} + +# load additional modules +use cf::pod; + END { cf::emergency_save } 1