--- deliantra/server/lib/cf.pm 2008/09/27 08:17:40 1.455 +++ deliantra/server/lib/cf.pm 2010/01/16 00:08:33 1.500 @@ -1,29 +1,30 @@ -# +# # This file is part of Deliantra, the Roguelike Realtime MMORPG. # # Copyright (©) 2006,2007,2008 Marc Alexander Lehmann / Robin Redeker / the Deliantra team # -# Deliantra is free software: you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. +# Deliantra is free software: you can redistribute it and/or modify it under +# the terms of the Affero GNU General Public License as published by the +# Free Software Foundation, either version 3 of the License, or (at your +# option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # -# You should have received a copy of the GNU General Public License -# along with this program. If not, see . +# You should have received a copy of the Affero GNU General Public License +# and the GNU General Public License along with this program. If not, see +# . # # The authors can be reached via e-mail to -# +# package cf; use 5.10.0; use utf8; -use strict "vars", "subs"; +use strict qw(vars subs); use Symbol; use List::Util; @@ -33,7 +34,9 @@ use Safe; use Safe::Hole; use Storable (); +use Carp (); +use Guard (); use Coro (); use Coro::State; use Coro::Handle; @@ -42,6 +45,7 @@ use Coro::Timer; use Coro::Signal; use Coro::Semaphore; +use Coro::SemaphoreSet; use Coro::AnyEvent; use Coro::AIO; use Coro::BDB 1.6; @@ -51,9 +55,8 @@ use JSON::XS 2.01 (); use BDB (); use Data::Dumper; -use Digest::MD5; use Fcntl; -use YAML (); +use YAML::XS (); use IO::AIO (); use Time::HiRes; use Compress::LZF; @@ -72,6 +75,9 @@ Compress::LZF::set_serializer "Storable", "Storable::net_mstore", "Storable::mretrieve"; Compress::LZF::sfreeze_cr { }; # prime Compress::LZF so it does not use require later +# strictly for debugging +$SIG{QUIT} = sub { Carp::cluck "SIGQUIT" }; + sub WF_AUTOCANCEL () { 1 } # automatically cancel this watcher on reload our %COMMAND = (); @@ -85,6 +91,8 @@ our $RELOAD; # number of reloads so far, non-zero while in reload our @EVENT; +our @REFLECT; # set by XS +our %REFLECT; # set by us our $CONFDIR = confdir; our $DATADIR = datadir; @@ -105,13 +113,15 @@ our $TICK = MAX_TIME * 1e-6; # this is a CONSTANT(!) our $NEXT_RUNTIME_WRITE; # when should the runtime file be written our $NEXT_TICK; -our $USE_FSYNC = 1; # use fsync to write maps - default off +our $USE_FSYNC = 1; # use fsync to write maps - default on our $BDB_DEADLOCK_WATCHER; our $BDB_CHECKPOINT_WATCHER; our $BDB_TRICKLE_WATCHER; our $DB_ENV; +our @EXTRA_MODULES = qw(pod match mapscript); + our %CFG; our $UPTIME; $UPTIME ||= time; @@ -132,6 +142,9 @@ our @POST_INIT; +our $REATTACH_ON_RELOAD; # set to true to force object reattach on reload (slow) +our $REALLY_UNLOOP; # never set to true, please :) + binmode STDOUT; binmode STDERR; @@ -142,6 +155,8 @@ $RUNTIME = <$fh> + 0.; } +eval "sub TICK() { $TICK } 1" or die; + mkdir $_ for $LOCALDIR, $TMPDIR, $UNIQUEDIR, $PLAYERDIR, $RANDOMDIR, $BDBDIR; @@ -151,6 +166,17 @@ ############################################################################# +%REFLECT = (); +for (@REFLECT) { + my $reflect = JSON::XS::decode_json $_; + $REFLECT{$reflect->{class}} = $reflect; +} + +# this is decidedly evil +$REFLECT{object}{flags} = { map +($_ => undef), grep $_, map /^FLAG_([A-Z0-9_]+)$/ && lc $1, keys %{"cf::"} }; + +############################################################################# + =head2 GLOBAL VARIABLES =over 4 @@ -204,42 +230,45 @@ =item @cf::INVOKE_RESULTS -This array contains the results of the last C call. When +This array contains the results of the last C call. When C is called C<@cf::INVOKE_RESULTS> is set to the parameters of that call. +=item %cf::REFLECT + +Contains, for each (C++) class name, a hash reference with information +about object members (methods, scalars, arrays and flags) and other +metadata, which is useful for introspection. + =back =cut -BEGIN { - *CORE::GLOBAL::warn = sub { - my $msg = join "", @_; +$Coro::State::WARNHOOK = sub { + my $msg = join "", @_; - $msg .= "\n" - unless $msg =~ /\n$/; + $msg .= "\n" + unless $msg =~ /\n$/; - $msg =~ s/([\x00-\x08\x0b-\x1f])/sprintf "\\x%02x", ord $1/ge; + $msg =~ s/([\x00-\x08\x0b-\x1f])/sprintf "\\x%02x", ord $1/ge; - LOG llevError, $msg; - }; -} + LOG llevError, $msg; +}; $Coro::State::DIEHOOK = sub { return unless $^S eq 0; # "eq", not "==" + warn Carp::longmess $_[0]; + if ($Coro::current == $Coro::main) {#d# warn "DIEHOOK called in main context, Coro bug?\n";#d# return;#d# }#d# # kill coroutine otherwise - warn Carp::longmess $_[0]; Coro::terminate }; -$SIG{__DIE__} = sub { }; #d#? - @safe::cf::global::ISA = @cf::global::ISA = 'cf::attachable'; @safe::cf::object::ISA = @cf::object::ISA = 'cf::attachable'; @safe::cf::player::ISA = @cf::player::ISA = 'cf::attachable'; @@ -262,7 +291,7 @@ } $EV::DIED = sub { - warn "error in event callback: @_"; + Carp::cluck "error in event callback: @_"; }; ############################################################################# @@ -330,7 +359,7 @@ =item my $lock = cf::lock_acquire $string Wait until the given lock is available and then acquires it and returns -a Coro::guard object. If the guard object gets destroyed (goes out of scope, +a L object. If the guard object gets destroyed (goes out of scope, for example when the coroutine gets canceled), the lock is automatically returned. @@ -346,51 +375,24 @@ =cut -our %LOCK; -our %LOCKER;#d# +our $LOCKS = new Coro::SemaphoreSet; sub lock_wait($) { - my ($key) = @_; - - if ($LOCKER{$key} == $Coro::current) {#d# - Carp::cluck "lock_wait($key) for already-acquired lock";#d# - return;#d# - }#d# - - # wait for lock, if any - while ($LOCK{$key}) { - #local $Coro::current->{desc} = "$Coro::current->{desc} "; - push @{ $LOCK{$key} }, $Coro::current; - Coro::schedule; - } + $LOCKS->wait ($_[0]); } sub lock_acquire($) { - my ($key) = @_; - - # wait, to be sure we are not locked - lock_wait $key; - - $LOCK{$key} = []; - $LOCKER{$key} = $Coro::current;#d# - - Coro::guard { - delete $LOCKER{$key};#d# - # wake up all waiters, to be on the safe side - $_->ready for @{ delete $LOCK{$key} }; - } + $LOCKS->guard ($_[0]) } sub lock_active($) { - my ($key) = @_; - - ! ! $LOCK{$key} + $LOCKS->count ($_[0]) < 1 } sub freeze_mainloop { tick_inhibit_inc; - Coro::guard \&tick_inhibit_dec; + &Guard::guard (\&tick_inhibit_dec); } =item cf::periodic $interval, $cb @@ -1172,7 +1174,10 @@ if (my $fh = aio_open "$filename~", O_WRONLY | O_CREAT, 0600) { aio_chmod $fh, SAVE_MODE; aio_write $fh, 0, (length $$rdata), $$rdata, 0; - aio_fsync $fh if $cf::USE_FSYNC; + if ($cf::USE_FSYNC) { + aio_sync_file_range $fh, 0, 0, IO::AIO::SYNC_FILE_RANGE_WAIT_BEFORE | IO::AIO::SYNC_FILE_RANGE_WRITE | IO::AIO::SYNC_FILE_RANGE_WAIT_AFTER; + aio_fsync $fh; + } aio_close $fh; if (@$objs) { @@ -1180,7 +1185,10 @@ aio_chmod $fh, SAVE_MODE; my $data = Coro::Storable::nfreeze { version => 1, objs => $objs }; aio_write $fh, 0, (length $data), $data, 0; - aio_fsync $fh if $cf::USE_FSYNC; + if ($cf::USE_FSYNC) { + aio_sync_file_range $fh, 0, 0, IO::AIO::SYNC_FILE_RANGE_WAIT_BEFORE | IO::AIO::SYNC_FILE_RANGE_WRITE | IO::AIO::SYNC_FILE_RANGE_WAIT_AFTER; + aio_fsync $fh; + } aio_close $fh; aio_rename "$filename.pst~", "$filename.pst"; } @@ -1189,8 +1197,11 @@ } aio_rename "$filename~", $filename; + + $filename =~ s%/[^/]+$%%; + aio_pathsync $filename if $cf::USE_FSYNC; } else { - warn "FATAL: $filename~: $!\n"; + warn "unable to save objects: $filename~: $!\n"; } } else { aio_unlink $filename; @@ -1291,6 +1302,8 @@ $EXTICMD{$name} = $cb; } +use File::Glob (); + cf::player->attach ( on_command => sub { my ($pl, $name, $params) = @_; @@ -1332,6 +1345,19 @@ }, ); +# "readahead" all extensions +sub cache_extensions { + my $grp = IO::AIO::aio_group; + + add $grp IO::AIO::aio_readdirx $LIBDIR, IO::AIO::READDIR_STAT_ORDER, sub { + for (grep /\.ext$/, @{$_[0]}) { + add $grp IO::AIO::aio_load "$LIBDIR/$_", my $data; + } + }; + + $grp +} + sub load_extensions { cf::sync_job { my %todo; @@ -1869,15 +1895,6 @@ "$UNIQUEDIR/$path" } -# and all this just because we cannot iterate over -# all maps in C++... -sub change_all_map_light { - my ($change) = @_; - - $_->change_map_light ($change) - for grep $_->outdoor, values %cf::MAP; -} - sub decay_objects { my ($self) = @_; @@ -1969,13 +1986,10 @@ $path = normalise $path, $origin && $origin->path; - cf::lock_wait "map_data:$path";#d#remove - cf::lock_wait "map_find:$path"; + my $guard1 = cf::lock_acquire "map_data:$path";#d#remove + my $guard2 = cf::lock_acquire "map_find:$path"; $cf::MAP{$path} || do { - my $guard1 = cf::lock_acquire "map_data:$path"; # just for the fun of it - my $guard2 = cf::lock_acquire "map_find:$path"; - my $map = new_from_path cf::map $path or return; @@ -1997,8 +2011,8 @@ } } -sub pre_load { } -sub post_load { } +sub pre_load { } +#sub post_load { } # XS sub load { my ($self) = @_; @@ -2053,8 +2067,6 @@ $self->fix_auto_apply; $self->update_buttons; cf::cede_to_tick; - $self->set_darkness_map; - cf::cede_to_tick; $self->activate; } @@ -2230,7 +2242,7 @@ return if $self->players; - warn "resetting map ", $self->path; + warn "resetting map ", $self->path, "\n"; $self->in_memory (cf::MAP_SWAPPED); @@ -2331,6 +2343,38 @@ ] } +=item cf::map::static_maps + +Returns an arrayref if paths of all static maps (all preinstalled F<.map> +file in the shared directory excluding F and F). May +block. + +=cut + +sub static_maps() { + my @dirs = ""; + my @maps; + + while (@dirs) { + my $dir = shift @dirs; + + next if $dir eq "/styles" || $dir eq "/editor"; + + my ($dirs, $files) = Coro::AIO::aio_scandir "$MAPDIR$dir", 2 + or return; + + for (@$files) { + s/\.map$// or next; + utf8::decode $_; + push @maps, "$dir/$_"; + } + + push @dirs, map "$dir/$_", @$dirs; + } + + \@maps +} + =back =head3 cf::object @@ -2405,7 +2449,7 @@ id => "say", title => "Map", reply => "say ", - tooltip => "Things said to and replied from npcs near you and other players on the same map only.", + tooltip => "Things said to and replied from NPCs near you and other players on the same map only.", }; our $CHAT_CHANNEL = { @@ -2535,17 +2579,18 @@ # use -1 or undef as default coordinates, not 0, 0 ($x, $y) = ($map->enter_x, $map->enter_y) - if $x <=0 && $y <= 0; + if $x <= 0 && $y <= 0; $map->load; $map->load_neighbours; return unless $self->contr->active; - $self->flag (cf::FLAG_DEBUG, 0);#d# temp - $self->activate_recursive; local $self->{_prev_pos} = $link_pos; # ugly hack for rent.ext $self->enter_map ($map, $x, $y); + + # only activate afterwards, to support waiting in hooks + $self->activate_recursive; } =item $player_object->goto ($path, $x, $y[, $check->($map)[, $done->()]]) @@ -2743,6 +2788,31 @@ $self->send_packet (sprintf "drawinfo %d %s", $flags || cf::NDI_BLACK, $text); } +=item $client->send_big_packet ($pkt) + +Like C, but tries to compress large packets, and fragments +them as required. + +=cut + +our $MAXFRAGSIZE = cf::MAXSOCKBUF - 64; + +sub cf::client::send_big_packet { + my ($self, $pkt) = @_; + + # try lzf for large packets + $pkt = "lzf " . Compress::LZF::compress $pkt + if 1024 <= length $pkt and $self->{can_lzf}; + + # split very large packets + if ($MAXFRAGSIZE < length $pkt and $self->{can_lzf}) { + $self->send_packet ("frag $_") for unpack "(a$MAXFRAGSIZE)*", $pkt; + $pkt = "frag"; + } + + $self->send_packet ($pkt); +} + =item $client->send_msg ($channel, $msg, $color, [extra...]) Send a drawinfo or msg packet to the client, formatting the msg for the @@ -2754,6 +2824,12 @@ # non-persistent channels (usually the info channel) our %CHANNEL = ( + "c/motd" => { + id => "infobox", + title => "MOTD", + reply => undef, + tooltip => "The message of the day", + }, "c/identify" => { id => "infobox", title => "Identify", @@ -2766,6 +2842,12 @@ reply => undef, tooltip => "Signs and other items you examined", }, + "c/shopinfo" => { + id => "infobox", + title => "Shop Info", + reply => undef, + tooltip => "What your bargaining skill tells you about the shop", + }, "c/book" => { id => "infobox", title => "Book", @@ -2790,12 +2872,42 @@ reply => undef, tooltip => "Shows which body parts you posess and are available", }, + "c/statistics" => { + id => "infobox", + title => "Statistics", + reply => undef, + tooltip => "Shows your primary statistics", + }, "c/skills" => { id => "infobox", title => "Skills", reply => undef, tooltip => "Shows your experience per skill and item power", }, + "c/shopitems" => { + id => "infobox", + title => "Shop Items", + reply => undef, + tooltip => "Shows the items currently for sale in this shop", + }, + "c/resistances" => { + id => "infobox", + title => "Resistances", + reply => undef, + tooltip => "Shows your resistances", + }, + "c/pets" => { + id => "infobox", + title => "Pets", + reply => undef, + tooltip => "Shows information abotu your pets/a specific pet", + }, + "c/perceiveself" => { + id => "infobox", + title => "Perceive Self", + reply => undef, + tooltip => "You gained detailed knowledge about yourself", + }, "c/uptime" => { id => "infobox", title => "Uptime", @@ -2814,6 +2926,14 @@ reply => "gsay ", tooltip => "Messages and chat related to your party", }, + "c/death" => { + id => "death", + title => "Death", + reply => undef, + tooltip => "Reason for and more info about your most recent death", + }, + "c/say" => $SAY_CHANNEL, + "c/chat" => $CHAT_CHANNEL, ); sub cf::client::send_msg { @@ -2828,17 +2948,14 @@ if ($CHANNEL{$channel}) { $channel = $CHANNEL{$channel}; - $self->ext_msg (channel_info => $channel) - if $self->can_msg; - + $self->ext_msg (channel_info => $channel); $channel = $channel->{id}; } elsif (ref $channel) { # send meta info to client, if not yet sent unless (exists $self->{channel}{$channel->{id}}) { $self->{channel}{$channel->{id}} = $channel; - $self->ext_msg (channel_info => $channel) - if $self->can_msg; + $self->ext_msg (channel_info => $channel); } $channel = $channel->{id}; @@ -2846,52 +2963,16 @@ return unless @extra || length $msg; - if ($self->can_msg) { - # default colour, mask it out - $color &= ~(cf::NDI_COLOR_MASK | cf::NDI_DEF) - if $color & cf::NDI_DEF; - - my $pkt = "msg " - . $self->{json_coder}->encode ( - [$color & cf::NDI_CLIENT_MASK, $channel, $msg, @extra] - ); - - # try lzf for large packets - $pkt = "lzf " . Compress::LZF::compress $pkt - if 1024 <= length $pkt and $self->{can_lzf}; - - # split very large packets - if (8192 < length $pkt and $self->{can_lzf}) { - $self->send_packet ("frag $_") for unpack "(a8192)*", $pkt; - $pkt = "frag"; - } + # default colour, mask it out + $color &= ~(cf::NDI_COLOR_MASK | cf::NDI_DEF) + if $color & cf::NDI_DEF; + + my $pkt = "msg " + . $self->{json_coder}->encode ( + [$color & cf::NDI_CLIENT_MASK, $channel, $msg, @extra] + ); - $self->send_packet ($pkt); - } else { - if ($color >= 0) { - # replace some tags by gcfclient-compatible ones - for ($msg) { - 1 while - s/([^<]*)<\/b>/[b]${1}[\/b]/ - || s/([^<]*)<\/i>/[i]${1}[\/i]/ - || s/([^<]*)<\/u>/[ul]${1}[\/ul]/ - || s/([^<]*)<\/tt>/[fixed]${1}[\/fixed]/ - || s/([^<]*)<\/fg>/[color=$1]${2}[\/color]/; - } - - $color &= cf::NDI_COLOR_MASK; - - utf8::encode $msg; - - if (0 && $msg =~ /\[/) { - # COMMAND/INFO - $self->send_packet ("drawextinfo $color 10 8 $msg") - } else { - $msg =~ s/\[\/?(?:b|i|u|fixed|color)[^\]]*\]//g; - $self->send_packet ("drawinfo $color $msg") - } - } - } + $self->send_big_packet ($pkt); } =item $client->ext_msg ($type, @msg) @@ -2904,10 +2985,10 @@ my ($self, $type, @msg) = @_; if ($self->extcmd == 2) { - $self->send_packet ("ext " . $self->{json_coder}->encode ([$type, @msg])); + $self->send_big_packet ("ext " . $self->{json_coder}->encode ([$type, @msg])); } elsif ($self->extcmd == 1) { # TODO: remove push @msg, msgtype => "event_$type"; - $self->send_packet ("ext " . $self->{json_coder}->encode ({@msg})); + $self->send_big_packet ("ext " . $self->{json_coder}->encode ({@msg})); } } @@ -2921,11 +3002,11 @@ my ($self, $id, @msg) = @_; if ($self->extcmd == 2) { - $self->send_packet ("ext " . $self->{json_coder}->encode (["reply-$id", @msg])); + $self->send_big_packet ("ext " . $self->{json_coder}->encode (["reply-$id", @msg])); } elsif ($self->extcmd == 1) { #TODO: version 1, remove unshift @msg, msgtype => "reply", msgid => $id; - $self->send_packet ("ext " . $self->{json_coder}->encode ({@msg})); + $self->send_big_packet ("ext " . $self->{json_coder}->encode ({@msg})); } } @@ -3062,7 +3143,7 @@ $SIG{FPE} = 'IGNORE'; $safe->permit_only (Opcode::opset qw( - :base_core :base_mem :base_orig :base_math + :base_core :base_mem :base_orig :base_math :base_loop grepstart grepwhile mapstart mapwhile sort time )); @@ -3076,6 +3157,7 @@ cf::object contr pay_amount pay_player map x y force_find force_add destroy insert remove name archname title slaying race decrease split + value cf::object::player player @@ -3091,9 +3173,9 @@ for ( ["cf::object" => qw(contr pay_amount pay_player map force_find force_add x y insert remove inv nrof name archname title slaying race - decrease split destroy change_exp)], + decrease split destroy change_exp value msg lore send_msg)], ["cf::object::player" => qw(player)], - ["cf::player" => qw(peaceful)], + ["cf::player" => qw(peaceful send_msg)], ["cf::map" => qw(trigger)], ) { no strict 'refs'; @@ -3121,8 +3203,10 @@ $qcode =~ s/"/‟/g; # not allowed in #line filenames $qcode =~ s/\n/\\n/g; + %vars = (_dummy => 0) unless %vars; + + my @res; local $_; - local @safe::cf::_safe_eval_args = values %vars; my $eval = "do {\n" @@ -3132,9 +3216,15 @@ . "\n}" ; - sub_generation_inc; - my @res = wantarray ? $safe->reval ($eval) : scalar $safe->reval ($eval); - sub_generation_inc; + if ($CFG{safe_eval}) { + sub_generation_inc; + local @safe::cf::_safe_eval_args = values %vars; + @res = wantarray ? $safe->reval ($eval) : scalar $safe->reval ($eval); + sub_generation_inc; + } else { + local @cf::_safe_eval_args = values %vars; + @res = wantarray ? eval eval : scalar eval $eval; + } if ($@) { warn "$@"; @@ -3195,9 +3285,11 @@ or cf::cleanup "$path: version mismatch, cannot proceed."; # patch in the exptable + my $exp_table = $enc->encode ([map cf::level_to_min_exp $_, 1 .. cf::settings->max_level]); $facedata->{resource}{"res/exp_table"} = { type => FT_RSRC, - data => $enc->encode ([map cf::level_to_min_exp $_, 1 .. cf::settings->max_level]), + data => $exp_table, + hash => (Digest::MD5::md5 $exp_table), }; cf::cede_to_tick; @@ -3209,8 +3301,8 @@ cf::face::set_visibility $idx, $info->{visibility}; cf::face::set_magicmap $idx, $info->{magicmap}; - cf::face::set_data $idx, 0, $info->{data32}, Digest::MD5::md5 $info->{data32}; - cf::face::set_data $idx, 1, $info->{data64}, Digest::MD5::md5 $info->{data64}; + cf::face::set_data $idx, 0, $info->{data32}, $info->{hash32}; + cf::face::set_data $idx, 1, $info->{data64}, $info->{hash64}; cf::cede_to_tick; } @@ -3244,29 +3336,13 @@ } { - # TODO: for gcfclient pleasure, we should give resources - # that gcfclient doesn't grok a >10000 face index. my $res = $facedata->{resource}; while (my ($name, $info) = each %$res) { if (defined $info->{type}) { my $idx = (cf::face::find $name) || cf::face::alloc $name; - my $data; - - if ($info->{type} & 1) { - # prepend meta info - - my $meta = $enc->encode ({ - name => $name, - %{ $info->{meta} || {} }, - }); - $data = pack "(w/a*)*", $meta, $info->{data}; - } else { - $data = $info->{data}; - } - - cf::face::set_data $idx, 0, $data, Digest::MD5::md5 $data; + cf::face::set_data $idx, 0, $info->{data}, $info->{hash}; cf::face::set_type $idx, $info->{type}; } else { $RESOURCE{$name} = $info; @@ -3360,11 +3436,13 @@ } sub reload_config { + warn "reloading config file...\n"; + open my $fh, "<:utf8", "$CONFDIR/config" or return; local $/; - *CFG = YAML::Load <$fh>; + *CFG = YAML::XS::Load scalar <$fh>; $EMERGENCY_POSITION = $CFG{emergency_position} || ["/world/world_105_115", 5, 37]; @@ -3378,6 +3456,8 @@ }; warn $@ if $@; } + + warn "finished reloading resource files\n"; } sub pidfile() { @@ -3399,6 +3479,15 @@ print $fh $$; } +sub main_loop { + warn "EV::loop starting\n"; + if (1) { + EV::loop; + } + warn "EV::loop returned\n"; + goto &main_loop unless $REALLY_UNLOOP; +} + sub main { cf::init_globals; # initialise logging @@ -3411,7 +3500,6 @@ cf::init_anim; cf::init_attackmess; cf::init_dynamic; - cf::init_block; $Coro::current->prio (Coro::PRIO_MAX); # give the main loop max. priority @@ -3427,12 +3515,13 @@ evthread_start IO::AIO::poll_fileno; cf::sync_job { + cf::load_settings; + cf::load_materials; + reload_resources; reload_config; db_init; - cf::load_settings; - cf::load_materials; cf::init_uuid; cf::init_signals; cf::init_commands; @@ -3447,12 +3536,13 @@ utime time, time, $RUNTIMEFILE; # no (long-running) fork's whatsoever before this point(!) + use POSIX (); POSIX::close delete $ENV{LOCKUTIL_LOCK_FD} if exists $ENV{LOCKUTIL_LOCK_FD}; (pop @POST_INIT)->(0) while @POST_INIT; }; - EV::loop; + main_loop; } ############################################################################# @@ -3557,39 +3647,49 @@ sub emergency_save() { my $freeze_guard = cf::freeze_mainloop; - warn "enter emergency perl save\n"; + warn "emergency_perl_save: enter\n"; cf::sync_job { + # this is a trade-off: we want to be very quick here, so + # save all maps without fsync, and later call a global sync + # (which in turn might be very very slow) + local $USE_FSYNC = 0; + # use a peculiar iteration method to avoid tripping on perl # refcount bugs in for. also avoids problems with players # and maps saved/destroyed asynchronously. - warn "begin emergency player save\n"; + warn "emergency_perl_save: begin player save\n"; for my $login (keys %cf::PLAYER) { my $pl = $cf::PLAYER{$login} or next; $pl->valid or next; delete $pl->{unclean_save}; # not strictly necessary, but cannot hurt $pl->save; } - warn "end emergency player save\n"; + warn "emergency_perl_save: end player save\n"; - warn "begin emergency map save\n"; + warn "emergency_perl_save: begin map save\n"; for my $path (keys %cf::MAP) { my $map = $cf::MAP{$path} or next; $map->valid or next; $map->save; } - warn "end emergency map save\n"; + warn "emergency_perl_save: end map save\n"; - warn "begin emergency database checkpoint\n"; + warn "emergency_perl_save: begin database checkpoint\n"; BDB::db_env_txn_checkpoint $DB_ENV; - warn "end emergency database checkpoint\n"; + warn "emergency_perl_save: end database checkpoint\n"; - warn "begin write uuid\n"; + warn "emergency_perl_save: begin write uuid\n"; write_uuid_sync 1; - warn "end write uuid\n"; + warn "emergency_perl_save: end write uuid\n"; + }; + + warn "emergency_perl_save: starting sync()\n"; + IO::AIO::aio_sync sub { + warn "emergency_perl_save: finished sync()\n"; }; - warn "leave emergency perl save\n"; + warn "emergency_perl_save: leave\n"; } sub post_cleanup { @@ -3637,6 +3737,8 @@ return if $RELOAD++; + my $t1 = EV::time; + while ($RELOAD) { warn "reloading..."; @@ -3706,7 +3808,7 @@ warn "unloading cf.pm \"a bit\""; delete $INC{"cf.pm"}; - delete $INC{"cf/pod.pm"}; + delete $INC{"cf/$_.pm"} for @EXTRA_MODULES; # don't, removes xs symbols, too, # and global variables created in xs @@ -3716,7 +3818,7 @@ warn "reloading cf.pm"; require cf; - cf::_connect_to_perl; # nominally unnecessary, but cannot hurt + cf::_connect_to_perl_1; warn "loading config and database again"; cf::reload_config; @@ -3724,14 +3826,16 @@ warn "loading extensions"; cf::load_extensions; - warn "reattaching attachments to objects/players"; - _global_reattach; # objects, sockets - warn "reattaching attachments to maps"; - reattach $_ for values %MAP; - warn "reattaching attachments to players"; - reattach $_ for values %PLAYER; + if ($REATTACH_ON_RELOAD) { + warn "reattaching attachments to objects/players"; + _global_reattach; # objects, sockets + warn "reattaching attachments to maps"; + reattach $_ for values %MAP; + warn "reattaching attachments to players"; + reattach $_ for values %PLAYER; + } - warn "running post_load"; + warn "running post_init jobs"; (pop @POST_INIT)->(1) while @POST_INIT; warn "leaving sync_job"; @@ -3745,6 +3849,9 @@ warn "reloaded"; --$RELOAD; } + + $t1 = EV::time - $t1; + warn "reload completed in ${t1}s\n"; }; our $RELOAD_WATCHER; # used only during reload @@ -3753,9 +3860,13 @@ # doing reload synchronously and two reloads happen back-to-back, # coro crashes during coro_state_free->destroy here. - $RELOAD_WATCHER ||= EV::timer $TICK * 1.5, 0, sub { - do_reload_perl; - undef $RELOAD_WATCHER; + $RELOAD_WATCHER ||= cf::async { + Coro::AIO::aio_wait cache_extensions; + + $RELOAD_WATCHER = EV::timer $TICK * 1.5, 0, sub { + do_reload_perl; + undef $RELOAD_WATCHER; + }; }; } @@ -3779,7 +3890,7 @@ our @WAIT_FOR_TICK_BEGIN; sub wait_for_tick { - return if tick_inhibit || $Coro::current == $Coro::main; + return Coro::cede if tick_inhibit || $Coro::current == $Coro::main; my $signal = new Coro::Signal; push @WAIT_FOR_TICK, $signal; @@ -3787,7 +3898,7 @@ } sub wait_for_tick_begin { - return if tick_inhibit || $Coro::current == $Coro::main; + return Coro::cede if tick_inhibit || $Coro::current == $Coro::main; my $signal = new Coro::Signal; push @WAIT_FOR_TICK_BEGIN, $signal; @@ -3924,7 +4035,8 @@ } # load additional modules -use cf::pod; +require "cf/$_.pm" for @EXTRA_MODULES; +cf::_connect_to_perl_2; END { cf::emergency_save }