hearse-1.5/0002775000175000000620000000000010215047703013553 5ustar roderickstaff00000000000000hearse-1.5/MANIFEST0000664000175000000620000000055607745306054014723 0ustar roderickstaff00000000000000 $Id: MANIFEST,v 1.4 2003/10/21 19:42:36 roderick Exp $ MANIFEST MANIFEST.SKIP Makefile.PL Notes README bones-info debian/changelog debian/control debian/copyright debian/hearse.config debian/hearse.cron.d debian/hearse.cron.daily debian/hearse.dirs debian/hearse.ppp.ip-up debian/hearse.postinst debian/hearse.templates debian/rules hearse hearse.conf hearse.spec hearse-1.5/MANIFEST.SKIP0000664000175000000620000000011507473004325015451 0ustar roderickstaff00000000000000# $Id: MANIFEST.SKIP,v 1.1 2002/05/22 21:08:37 roderick Exp $ ~$ ^Makefile$ hearse-1.5/Makefile.PL0000664000175000000620000000141607477027571015546 0ustar roderickstaff00000000000000# $Id: Makefile.PL,v 1.3 2002/06/04 03:09:45 roderick Exp $ use ExtUtils::MakeMaker; WriteMakefile( EXE_FILES => [qw(bones-info hearse)], NAME => 'hearse', PREREQ_PM => { 'Digest::MD5' => 0, 'Errno' => 0, 'HTTP::Request' => 0, 'LWP::UserAgent' => 0, }, VERSION_FROM => 'hearse', ); package MY; # You can specify the MAN1EXT as an argument to WriteMakefile(), but you # get a warning. Doing it this way lets me be a little smarter about # overriding your value (which perhaps should be left as-is if it's 1n, # eg). sub constants { my ($self, @arg) = @_; $self->{MAN1EXT} =~ s/^1p\z/1/; return $self->SUPER::constants(@arg); } sub dist_core { return q(dist:; @echo "use dpkg-buildpackge, not make dist" >&2; false); } hearse-1.5/Notes0000664000175000000620000002247510215047643014601 0ustar roderickstaff00000000000000todo -------------------------------------------------------------------------- - support for X_MATCHBONES /m - for each bones file that you upload, it will attempt to download one of the same level. (i.e. if you upload a level 4 bones file you will get a different level 4 bones file in exchange). If no bones file is available for that level, it will revert to the default behaviour and download a random level instead. If you wish to support this behaviour for the Unix client, here's how it works. The filenames of all accepted bones files are stuffed into a stack. Each time round the download loop, the top filename is popped off the stack and passed as an argument to the X_MATCHBONES header, e.g. sending X_MATCHBONES:bonD0.4 will attempt to download bonD0.4 if possible. - support for skipping bonescheck The idea behind the 'bonescheck' phase was that it'd save network time to send the MD5 of the file and the version info to decide whether the server will accept it. However, in the common case it will always ask for the full file to be sent. So simply eliminating the bonescheck step increases speed by ~25% (probably not so much on Linux systems since the bones files are bigger, and it's also less important since you have it set up to run under cron). If there is a problem with the full bones file, info error messages will be sent back exactly as they are from the old bonescheck - switch to reset/ignore timestamp? /r - resets the 'last uploaded' timestamp (that is, prevents Hearse attempting to upload old bones files). Included as a workaround for users who sometimes run into problems with this. - merge in Crawl-modified version of hearse - Windows support (see message from Darshan Shaligram from 2003-04-01 in +project/hearse) - run evey 15 minutes by default, so you don't miss a bones which is created then used before the end of the day - man page - cron.d - cron.daily - code to ensure version in hearse.spec, debian/changelog, hearse match - simple install instructions for dependencies - scripts to publish new versions to web page - bump version: hearse, hearse.spec, debian/changelog - tell Alexis about new X_HEARSECRC: echo -n "UNIX-HEARSE $version" | md5sum - cvs-buildpackage - move generated files to ../web/dist - pod2html hearse >! ../web/hearse.html - pod2html bones-info >! ../web/bones-info.html - cd ../web/dist - ln -s hearse_$version.tar.gz hearse-$version.tar.gz - ln -sf hearse_$version.tar.gz hearse-current.tar.gz - run-scanpackages - rpm -tb --sign hearse-$version.tar.gz && mv -i /usr/src/rpm/RPMS/noarch/hearse-$version-1.noarch.rpm . - update web/bones-info - update version in index.html.m4, gmake - gmake -C ../web install - rpm: Is there a way to do interactive configuration at install time? - rpm: How do Red Hat people do the equivalent of /etc/ppp/ip-up.d? ------------------------------------------------------------------------------- Message-ID: <014601c1fb74$755caaf0$8b0224d9@bingalybong> From: "Alexis Manning" To: "Roderick Schertler" References: Subject: Re: Is Hearse safe to use with 3.4.0? Date: Tue, 14 May 2002 19:23:34 +0100 From: "Roderick Schertler" > At the risk of invoking the Curse of Hearse, I'm interested in writing a > Unix version, particularly if it can use your server. Would you send me > what info I'd need to interoperate with the server, or the source for > the client? Thanks very much. A brave man! Well, someone else has offered and is currently (hopefully) perusing the source code with an eye to get a-porting. So we have various options: * You could offer to work on it with him * You could wait and see if he flakes / if the Curse of Hearse zaps him * You could work on it separately (Although I will of course let him know if you want to do that. Not fair to keep people in the dark) I am blissfully indifferent, I don't care particularly much how it's done as long as it's done. So let me know what you'd prefer. In the meantime, here is some light music. Sorry, no. In the meantime, here is some stuff I've worked about about Windows/Unix differences for Hearse which you may find interesting, or may scare you off the task altogether. Cheers, -- A. ==== How Hearse works: -------------------- The Hearse client is simple. A couple of bits of data are stored in the registry, including the date the user last uploaded bones, and a per-user token. If the user hasn't used Hearse before, he is prompted for his email address and the server offers him a per-user token in exchange. This is stuffed into the registry and sent with all subsequent requests, and is used to track who uploaded what bones file (so no one gets the same bones file twice), and used to selectively block abusive users of the system. The per-user token is 32 characters of gibberish, if you care. After that's out of the way, the process is dirt simple: Loop over all bones files that are newer than the last bones date Generate an MD5 hash for that file Ask the server if it's OK If the server says "Yup", submit the bones file End loop If any bones files were submitted Loop Ask the server for a bones file Repeat until it says No Update the last bones date That's it. It does get a leetle more complicated, but not much. A list of all bones files in that directory is generated and sent to the server during the download phase so you're not offered a bones file that you already have. All the communication is done by hitting variations of a fixed URL and passing arguments as HTTP headers. Any errors (e.g. "No more bones for you") are handled by the server passing back a header that indicates that the body is an error message that should be displayed. Headers are: X_USERTOKEN: per user token X_HEARSECRC: MD5 hash of the Hearse executable, used to determine whether a newer version is available. We'll work out something appropriate to be sent for Unix. Sending 'noupdate' will prevent any update being offered. X_BONESCRC: MD5 hash of the current bones file X_VER1 Ugly and non-extensible way of sending the bonesfile "magic number" information. X_VER2 Done this way to allow for the possibility of masking the parts of the version X_VER3 struct that don't really matter at some point in the dim and distant. X_VER4 X_ERROR Was there an error? If present, is the body some text that should be displayed (INFO), or should the body be displayed then the client terminate? (FATAL) X_FILENAME Name of the bonesfile in the body of the message X_USERLEVELS Comma delimited listed of the bonesfiles that the user already has -- don't offer them bonD0.5 if they already have a bonD0.5 X_GIVEINFO A crappily named header. Not currently used, but set with the uploaded first bones file of that session. The idea is that it could be used to trigger a Message of the Day or some such. X_CLIENTID Always set to PC-HEARSE, unused. For future expansion. Yours could say "UNIX-HEARSE", so if different server behaviour was needed for the two clients, it could be easily coded. X_FORCEUPDATE Server demands that you update to the latest version to use Hearse. Windows versions auto-update. Hearse and Unix ----------------- The *'s indicate current thinking on the subject, not set in stone at all. Things in square brackets are really server-side issues that I've just put in to remind me. Let me know any particular part makes no sense, or if it sounds like I've missed something. Hearse stores a per-user token which is sent with every request to the server. This is so that people don't download the same bones file more than once. It's also used to track users in case of abuse. Should this be per-site on Unix rather than per-user? * Per-site is probably better. The only user who can create a wizard bones file under Unix is root, so it's likely to be safe. Worst case, whole site has to be banned. File ownership issues * Hearse will likely have to run setuid nethack so it can access bones files. Hearse has AutoUpdate functionality. Best way to handle this under Unix? * Simplest would probably be to display the error message returned from the server and quit. They'll have to download the latest source. [Also worth having the server email all the users when an incompatible version of Hearse is released] Waaah! Byte ordering * Unlikely to be a problem since the magic numbers will also have their byte order buggered. Just make sure the version info is read in byte by byte, not DWORD by DWORD :) Hearse stores various bits of data in the registry. What to do? * Have a /etc/hearse.conf file that stores: - location of nethack bones - Hearse server address [note: this is hardcoded into the client for the Windows version. Ewww] - File access/owner modes to set on downloaded bones - Date of last bones upload * Possible race conditions on this file if multiple users run Hearse simultaneously? ------------------------------------------------------------------------------- $Id: Notes,v 1.13 2005/03/13 14:35:15 roderick Exp $ hearse-1.5/README0000664000175000000620000000505310215026450014430 0ustar roderickstaff00000000000000This is the Unix Hearse distribution. Nethack sometimes saves the level on which you die (including your stuff, what killed you, and your ghost) in a "bones file". These files get loaded into later Nethack games. If you're the only Nethack player on your system you'll only get bones files you created yourself. With Hearse, you can automatically exchange bones files with other Nethack players. When run it uploads any new bones files it finds on your system, then downloads any bones files the server feels like giving it. See http://www.argon.org/~roderick/hearse/ for more information. An important thing to note is that by default using Hearse will cause you to end up with more bones than you otherwise would have. This changes the game's balance and is considered by many players to be a mild form of cheating. You can address this by turning on the --delete-uploaded option, but the down side is you'll never encounter your own bones files. The change log is in debian/changelog. Other than this you'll probably want to ignore the contents of the debian subdirectory, it contains the files which turn the distribution into a Debian package. Prerequisites are: - the libwww-perl package - either Digest::MD5 or the older MD5 module The program can be installed in the usual manner: perl Makefile.PL make install See the man page for information about configuring hearse and setting it up to run automatically. You can run it from this directory as-is for testing. In this case you might like to use `perldoc ./hearse' or the like to read the documentation. There's a sample configuration file in hearse.conf, but it isn't installed by "make install" (it isn't required for hearse to run). Roderick Schertler Copyright (C) 2002 Roderick Schertler. This program 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 2 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. For a copy of the GNU General Public License write to the Free Software Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA On Debian systems, the complete text of the GNU General Public License can be found in `/usr/doc/copyright/GPL'. $Id: README,v 1.4 2005/03/13 12:08:08 roderick Exp $ hearse-1.5/bones-info0000775000175000000620000002732510215026365015550 0ustar roderickstaff00000000000000#!/usr/bin/perl -w use strict; # $Id: bones-info,v 1.6 2005/03/13 12:07:17 roderick Exp $ # # Roderick Schertler # # Print some info about a Nethack bones file. # Copyright (C) 2002 Roderick Schertler # # This program 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 2 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. # # For a copy of the GNU General Public License write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA use Getopt::Long (); # BS == byte sex sub BS_AUTO () { 0 } sub BS_LITTLE () { 1 } sub BS_BIG () { 2 } (my $Me = $0) =~ s-.*/--; my $Byte_sex = BS_LITTLE; my $Debug = 0; my $Exit = 0; my $Num_fmt = 'u'; my $Verbose = 0; my $Version = q$Revision: 1.6 $ =~ /(\d\S+)/ ? $1 : '?'; my @Option_spec = ( 'auto|a' => sub { $Byte_sex = BS_AUTO }, 'big-endian|b' => sub { $Byte_sex = BS_BIG }, 'debug!' => \$Debug, 'help' => sub { usage() }, 'hexadecimal|x' => sub { $Num_fmt = 'x' }, 'little-endian|l' => sub { $Byte_sex = BS_LITTLE }, 'verbose|v' => \$Verbose, 'version' => sub { print "$Me version $Version\n"; exit }, ); my $Usage = <import(2.11); # I'm setting this environment variable lest he sneaks more bad # defaults into the module. local $ENV{POSIXLY_CORRECT} = 1; Getopt::Long::config qw( default no_autoabbrev no_getopt_compat require_order bundling no_ignorecase ); return Getopt::Long::GetOptions @_; } sub init { getopt @Option_spec or usage; } # Decode raw version info values. sub decode_version { my @v = @_; my (@v1, @v2, @v3, @v4); $v1[V1_VERSION_MAJOR] = ($v[1] & (255 << 24)) >> 24; $v1[V1_VERSION_MINOR] = ($v[1] & (255 << 16)) >> 16; $v1[V1_PATCH_LEVEL] = ($v[1] & (255 << 8)) >> 8; $v1[V1_EDIT_LEVEL] = ($v[1] & 255); @v2 = split //, unpack "b*", pack "V", $v[2]; $v3[V3_ARTIFACTS] = ($v[3] & (255 << 24)) >> 24; $v3[V3_OBJECTS] = ($v[3] & (4095 << 12)) >> 12; $v3[V3_MONSTERS] = ($v[3] & 4095); $v4[V4_FLAG] = ($v[4] & (255 << 24)) >> 24; $v4[V4_OBJ] = ($v[4] & (127 << 17)) >> 17; $v4[V4_MONST] = ($v[4] & (127 << 10)) >> 10; $v4[V4_YOU] = ($v[4] & 1023); return $v[0], \@v1, \@v2, \@v3, \@v4; } # Return true if it looks like the given version info is invalid. sub invalid_version { my (@vdec) = decode_version @_; return 1 if $vdec[1][V1_VERSION_MAJOR] < 3; return 1 if $vdec[4][V4_MONST] > 1000; return 1 if grep { $vdec[2][$_] && $Feature[$_] =~ /^UNKNOWN/ } 0..$#{ $vdec[2] }; return 0; } # Output version info info in verbose form. sub verbose { my ($file, $size, @vin) = @_; my @vdec = decode_version @vin; print "$file: $size bytes\n"; printf " read as: %s endian\n", $vin[0]; printf " incarnation: %-10$Num_fmt (%s)\n", $vin[1], join '.', @{ $vdec[1] }; my $l = sprintf " feature_set: %-10$Num_fmt (", $vin[2]; my @f = map { $Feature[$_] } grep { $vdec[2][$_] } 0..$#Feature; my $w = 78; while (@f) { my $s = "$l" . shift @f; # always eat at least 1 @f $s .= ' ' . shift @f while @f && length("$s $f[0]") <= $w; $s .= ")" unless @f; print "$s\n"; $l = ' ' x length $l; } print "$l)\n" if $l =~ /\S/; # no feature were set printf " entity_count: %-10$Num_fmt (%s)\n", $vin[3], join ', ', "$vdec[3][V3_ARTIFACTS] artifacts", "$vdec[3][V3_OBJECTS] objects", "$vdec[3][V3_MONSTERS] monsters"; printf " struct_sizes: %-10$Num_fmt (%s)\n", $vin[4], join ', ', "$vdec[4][V4_FLAG] flag", "$vdec[4][V4_OBJ] obj", "$vdec[4][V4_MONST] monst", "$vdec[4][V4_YOU] you"; print "\n"; } sub one_file { my ($file) = @_; my $open = $file; $open = "gzip -dc \Q$file\E |" if $file =~ /\.(gz|z|Z)\z/; if (!open FILE, $open) { warn "$Me: can't read $open: $!\n"; $Exit ||= 1; return; } my $data = do { local $/; }; my $size = length $data; my @v_little = (little => unpack 'V' x 4, $data); my @v_big = (big => unpack 'N' x 4, $data); my @v; if ($Byte_sex == BS_AUTO) { my $good_little = !invalid_version @v_little; my $good_big = !invalid_version @v_big; if (!($good_little ^ $good_big)) { warn "$Me: can't intuit byte sex of $file\n"; $Exit ||= 1; return; } @v = $good_little ? @v_little : @v_big; } else { @v = $Byte_sex == BS_LITTLE ? @v_little : @v_big; } if ($Verbose) { verbose $file, $size, @v; } else { my $n = "%-10$Num_fmt"; printf "%-11s sex=%s v1=$n v2=$n v3=$n v4=$n\n", $file, substr($v[0], 0, 1), @v[1..4]; } } sub main { init; @ARGV or die "$Me: no files specified\n"; one_file $_ for @ARGV; return 0; } $Exit = main || $Exit; $Exit = 1 if $Exit && !($Exit % 256); exit $Exit; __END__ =head1 NAME bones-info - display information about a Nethack bones file =head1 SYNOPSIS B [B<-a | --auto>] [B<-b | --big-endian>] [B<--debug>] [B<--help>] [B<-x | --hexadecimal>] [B<-l | --little-endian>] [B<-v | --verbose>] [B<--version>] I... =head1 DESCRIPTION B displays information about a Nethack bones file. By default it shows what byte sex it used to read the file and the 4 version numbers which constitute the feature set and platform for the Nethack binary which generated it. =head1 ENDIANNESS (aka BYTE SEX) Normally B reads the bones file in little endian order, regardless of the byte sex of the current system, mostly because it was originally written to help with diagnosing problems with L and that's the most useful behavior for that purpose. You can use the B<--auto>, B<--big-endian>, and B<--little-endian> switches to change this. B<--auto> is particularly useful (and appropriate) when using B<--verbose>. =head1 OPTIONS =over 4 =item B<-a>, B<--auto> Try to guess the right byte sex (little endian or big endian) for each input file. If there doesn't seem to be a right choice, B will output a warning, set a non-zero exit status, and move on to the next file. =item B<-b>, B<--big-endian> Read the bones files in big endian order, such as is used by Macs. See also L. =item B<--debug> Turn debugging on. =item B<--help> Show the usage message and die. =item B<-x>, B<--hexadecimal> Output numbers in hexadecimal form. =item B<-l>, B<--little-endian> Read the bones files in little endian order, such as is used by Intel hardware. This is the default, I include it so that you don't have to check what the default is if you know you want it a certain way. =item B<-v>, B<--verbose> Output more info about the bones file. This tries to decode the 4 version numbers. Its useful when you want to see what the differences are between two sets of version numbers. You'd normally want to use B<--auto> when you use B<--verbose>. =item B<--version> Show the version number and exit. =back =head1 EXAMPLES Output the values as used by the L server: $ bones-info * bonD0.0 sex=l v1=1 v2=2 v3=3 v4=4 bonD0.4.gz sex=l v1=50593792 v2=10357958 v3=555422078 v4=2759955912 bonD0.8.Z sex=l v1=1027 v2=3322682880 v3=2115050273 v4=3365241252 bonD0.19 sex=l v1=50528512 v2=10357830 v3=555409789 v4=2558629316 bonM0.1 sex=l v1=50593792 v2=404622406 v3=555417981 v4=2759955916 bonM0.T sex=l v1=50593792 v2=1969222 v3=555417981 v4=2759955912 Output the real values as seen on the system which wrote the file (by guessing the byte sex of the file): $ bones-info --auto * bones-info: can't intuit byte sex of bonD0.0 bonD0.4.gz sex=l v1=50593792 v2=10357958 v3=555422078 v4=2759955912 bonD0.8.Z sex=b v1=50593792 v2=1969350 v3=555422078 v4=2759955912 bonD0.19 sex=l v1=50528512 v2=10357830 v3=555409789 v4=2558629316 bonM0.1 sex=l v1=50593792 v2=404622406 v3=555417981 v4=2759955916 bonM0.T sex=l v1=50593792 v2=1969222 v3=555417981 v4=2759955912 zsh: exit 1 bones-info --auto * Decode the version numbers: $ bones-info --auto --verbose bonD0.4.gz bonD0.8.Z bonD0.4.gz: 18389 bytes read as: little endian incarnation: 50593792 (3.4.0.0) feature_set: 10357958 (REINCARNATION SINKS KOPS MAIL TOURIST STEED TEXTCOLOR INSURANCE ELBERETH EXP_ON_BOTL TIMED_DELAY) entity_count: 555422078 (33 artifacts, 433 objects, 382 monsters) struct_sizes: 2759955912 (164 flag, 64 obj, 101 monst, 456 you) bonD0.8.Z: 22296 bytes read as: big endian incarnation: 50593792 (3.4.0.0) feature_set: 1969350 (REINCARNATION SINKS KOPS MAIL TOURIST STEED TEXTCOLOR INSURANCE ELBERETH EXP_ON_BOTL) entity_count: 555422078 (33 artifacts, 433 objects, 382 monsters) struct_sizes: 2759955912 (164 flag, 64 obj, 101 monst, 456 you) $ _ =head1 BUGS Unsigned longs are assumed to be 4 bytes. The --auto byte sex detection isn't robust. It'd be nice to be provide --verbose output for bones files from older versions. =head1 AVAILABILITY This program is distributed with the Unix Hearse client. The code is licensed under the GNU GPL. Check http://www.argon.org/~roderick/hearse/ for updated versions. =head1 AUTHOR Roderick Schertler =cut hearse-1.5/hearse0000775000175000000620000012025510215047643014756 0ustar roderickstaff00000000000000#!/usr/bin/perl -w use strict; # $Id: hearse,v 1.16 2005/03/13 14:35:15 roderick Exp $ # # Roderick Schertler # Copyright (C) 2002 Roderick Schertler # # This program 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 2 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. # # For a copy of the GNU General Public License write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA use Errno qw(ENOENT); use Fcntl qw(LOCK_EX LOCK_NB O_CREAT O_WRONLY); use File::Basename qw(basename dirname); use Getopt::Long (); use HTTP::Request (); use LWP::UserAgent (); # configuration my @Bones_dir = qw(/var/games/nethack /usr/games/lib/nethackdir .); my $Bones_dir_file = 'record'; # file always present in a bones dir my $Bones_mode = '660'; my $Config_file = '/etc/nethack/hearse.conf'; my @Compress_suffix = qw(.gz .z .Z); my $Decompress_cmd = 'gzip -dc'; my $Last_run_stamp_file = '.hearse.timestamp'; # rel. to bones dir, or absolute my $Lock_tries = 900; my $Lock_sleep = 1; my @Role = qw(Arc Bar Cav Hea Kni Mon Pri Rog Ran Sam Tou Val Wiz); my $Run_as_user = 'games'; my $Run_as_group = 'games'; my $Server_url = 'http://hearse.krollmark.com/bones.dll'; my $User_token_file = '/etc/nethack/hearse.user-token'; # globals unlikely to need configuration my # new line required for makemaker $VERSION = '1.5'; my $Bones_dir = undef; my $Debug = 0; my $Do_help = 0; my $Do_version = 0; my $Delete_uploaded = 0; my $Exit = 0; my $Force_download = 0; # intentionally undocumented my @Local_bones = (); my $Lock_file = undef; my $Lwp_ua = undef; # LWP::UserAgent object my $Me = basename $0; my $Quiet = 0; my $Quiet_cron = 0; my $Role_re = do { my $r = join '|', map { quotemeta } @Role; qr/(?:$r)/ }; my $Run_as_me = 0; my $User_email = undef; # only used if there's no token already my $User_token = undef; my %Option_spec = ( # short name, type, allow in config, var 'bones-dir' => ['b', 's', 1, \$Bones_dir], 'bones-mode' => [ '', 's', 1, \$Bones_mode], 'config-file' => ['c', 's', 0, \$Config_file], 'cron' => [ '', '', 1, \$Quiet_cron], 'debug' => [ '', '', 1, \$Debug], 'delete-uploaded' => [ '', '', 1, \$Delete_uploaded], 'help' => [ '', '', 0, \$Do_help], 'force-download' => [ '', '', 0, \$Force_download], 'lock-file' => [ '', 's', 1, \$Lock_file], 'quiet' => ['q', '', 1, \$Quiet], 'run-as-me' => [ '', '', 1, \$Run_as_me], 'run-as-user' => [ '', 's', 1, \$Run_as_user], 'run-as-group' => [ '', 's', 1, \$Run_as_group], 'server-url' => [ '', 's', 1, \$Server_url], 'stamp-file' => [ '', 's', 1, \$Last_run_stamp_file], 'user-email' => [ '', 's', 1, \$User_email], 'user-token' => [ '', 's', 1, \$User_token], 'user-token-file' => [ '', 's', 1, \$User_token_file], 'version' => [ '', '', 0, \$Do_version], ); my $Usage = <(); } xdie "can't find a $name directory, checked: @d\n"; } # Ensure that a given directory exists, or die. sub mkpath { my ($dir) = @_; return if -d $dir; require File::Path; File::Path::mkpath($dir, 1); } # Return an MD5 hash in hex, using whatever module is available. { my $md5_sub; sub md5 { my ($rdata) = @_; if (!$md5_sub) { if (eval { require Digest::MD5 }) { debug 'md5 using Digest::MD5'; $md5_sub = \&Digest::MD5::md5_hex; } elsif (eval { require MD5 }) { debug 'md5 using MD5'; $md5_sub = sub { MD5->hexhash(shift) }; } else { xdie "can't find Perl module to make MD5 hashes,", " install either Digest::MD5 or MD5"; } } return $md5_sub->($$rdata); } } # Remove leading and trailing space from STR and return it. sub trim { my ($s) = @_; $s =~ s/^\s+//; $s =~ s/\s+$//; return $s; } # Getopt::Long has some really awful defaults. This function configures # it to use more sane settings. sub getopt { Getopt::Long->import(2.11); # I'm setting this environment variable lest he sneaks more bad # defaults into the module. local $ENV{POSIXLY_CORRECT} = 1; Getopt::Long::config qw( default no_autoabbrev no_getopt_compat require_order bundling no_ignorecase ); return Getopt::Long::GetOptions @_; } # Turn a text description into a boolean. sub boolean { my ($val) = @_; if ($val =~ /^(on|true|yes|1)\z/i) { return 1; } elsif ($val =~ /^(off|false|no|0)\z/i) { return 0; } else { return; } } # Read the config file, updating globals appropriately. sub process_config_file { local *FILE; debug "config file $Config_file"; if (!open FILE, $Config_file) { return if $! == ENOENT; xdie "can't read $Config_file:"; } local $. = 0; my $choke = sub { xdie @_, " at $Config_file line $.\n"; }; while () { next if /^\s*#/; next if /^\s*$/; my ($var, $val) = map { trim $_ } split ' ', $_, 2; debug "config [$var] = [$val]"; my $spec = $Option_spec{$var} or $choke->("invalid config file option `$var'"); my ($short, $type, $in_config, $ref) = @$spec; $in_config or $choke->("$var can only be specified as a command line switch"); if ($type eq 's') { if ($val eq '-') { $val = ''; } elsif ($val eq '') { $choke->("no value specified for $var", " (use `-' to turn a string setting off)"); } } elsif ($type eq '') { $val = boolean $val; defined $val or $choke->("invalid boolean value for $var,", " use on/off/true/false/yes/no/1/0"); } else { xdie "invalid variable type `$type'\n"; } $$ref = $val; } close FILE or xdie "error closing $Config_file:"; } sub ugid { my $rgid = (split ' ', $()[0]; my ($egid, $supgroups) = split ' ', $), 2; $supgroups = 'undef' if !defined $supgroups; return "ruid $< euid $> rgid $rgid egid $egid supgroups $supgroups"; } # Set the real/effective user/group ids according to the $Run_as_* # variables, or die trying. sub set_ugid { debug "set_ugid before ", ugid; return if $Run_as_me; if ($Run_as_group ne '') { my $gid = getgrnam $Run_as_group; defined $gid or xdie "invalid --run-as-group `$Run_as_group'\n"; my $want = "$gid $gid"; $( = $gid; $) = $want; $( eq $want or xdie "error setting real gid (want $want, got $()\n"; $) eq $want or xdie "error setting effective gid (want $want, got $))\n"; } if ($Run_as_user ne '') { my $uid = getpwnam $Run_as_user; defined $uid or xdie "invalid --run-as-user `$Run_as_user'\n"; $> = $uid; $< = $uid; $< eq $uid or xdie "error setting real uid (want $uid, got $<)\n"; $> eq $uid or xdie "error setting effective uid (want $uid, got $>)\n"; } debug "set_ugid after ", ugid; } # Process args and do some other initializations. sub init { # My output is often going to a non-terminal (cron); flush stdout so # it stays in the right order with stderr. $| = 1; # I want to process the configuration file before the command line # args, so that the command line can override settings from the # config file. I need to process the command line to see if she # overrode the config file, however. So, I actually parse the # command line args twice, both before and after reading the config # file. This is okay because none of the command line args have # any side effects which go wrong when done twice. if (@ARGV) { my @opt; for my $name (keys %Option_spec) { my ($short, $type, $in_config, $ref) = @{ $Option_spec{$name} }; $short = "|$short" if $short ne ''; $type = "=$type" if $type ne ''; push @opt, "$name$short$type" => $ref; } my @orig_argv = @ARGV; getopt @opt or usage; process_config_file; @ARGV = @orig_argv; getopt @opt or usage; # Allow - to mean '' on the command line, as it does in a config # file. for (values %Option_spec) { my $r = $_->[3]; $$r = '' if defined $$r && $$r eq '-'; } } else { process_config_file; } usage if $Do_help; if ($Do_version) { print "$Me version $VERSION\n"; exit; } if ($Debug) { require LWP::Debug; LWP::Debug::level('+'); } if (defined $Bones_dir) { -d $Bones_dir or xdie "--bones-dir $Bones_dir isn't a directory\n"; } else { $Bones_dir = first_dir 'bones', sub { -f "$_/$Bones_dir_file" }, @Bones_dir; } debug "bones-dir $Bones_dir"; my $mode = oct $Bones_mode or xdie "invalid --bones-mode $Bones_mode\n"; $Bones_mode = $mode; # If the timestamp file was specified with a relative path, put it # in the bones directory. That's the most useful behavior when # running against multiple versions of the game. $Last_run_stamp_file = "$Bones_dir/$Last_run_stamp_file" unless $Last_run_stamp_file =~ /^\//; # XXX unless file_name_is_absolute $Last_run_stamp_file; # Without this HTTP::Headers will turn the _ into -. $HTTP::Headers::TRANSLATE_UNDERSCORE = 0; $Lwp_ua = LWP::UserAgent->new; $Lwp_ua->agent("$Me/$VERSION " . $Lwp_ua->agent); $Lwp_ua->env_proxy; } # Create an HTTP::Request object for the given SERVER_CMD_*. { my $first_req = 1; my $client_crc; # postpone calculation until after $Debug is set sub make_req { my ($cmd) = @_; $client_crc ||= md5 \do { F_CLIENT . " $VERSION" }; my $uri = URI->new($Server_url); $uri->query_form(act => $cmd); my $req = HTTP::Request->new( ($cmd eq SERVER_CMD_UPLOAD ? 'POST' : 'GET'), $uri); $req->header(HEADER_HEARSE_CRC, $client_crc); $req->header(HEADER_CLIENT, F_CLIENT); $req->header(HEADER_TOKEN, $User_token) if defined $User_token; $req->header(HEADER_WANTS_INFO, 'Y') if $first_req; $first_req = 0; return $req; } } # Take a header value or undef, return the string with leading and # trailing spaces chopped, turning undef into ''. sub clean_header { my ($s) = @_; return '' if !defined $s; $s =~ s/^\s+//; $s =~ s/\s+$//; return $s; } # Output an HTTP::Message (a request or response) for debugging. sub debug_http_message { my ($r) = @_; return unless $Debug; my $s = ''; if ($r->can('status_line')) { $s .= sprintf "<- %s\n", $r->status_line; } else { $s .= sprintf "-> %s %s\n", $r->method, $r->uri; } for (split /\n/, $r->headers_as_string) { $s .= " $_\n"; } my $body = $r->content; if (defined $body && $body ne '') { $s .= " content:\n"; $body =~ s/([^\x20-\x7e])/sprintf '%%%02X', ord $1/eg; my $lines = 0; while ($body ne '') { $s .= sprintf " %s\n", substr $body, 0, 70, ''; $body = '[elided]' if ++$lines == 10; } } chomp $s; debug $s; } # Send a request to the server. If this fails, die. If the response # contains an error or warning, display it. If it was a fatal error, # die. Return a boolean telling whether there was a (non-fatal) error, # and a response object. sub server_cmd { my ($req) = @_; debug_http_message $req; my $resp = $Lwp_ua->request($req); debug_http_message $resp; if (!$resp->is_success) { xdie "error contacting server: ", $resp->status_line, "\n"; } my $update = clean_header $resp->header(HEADER_FORCE_UPDATE); my $error = clean_header $resp->header(HEADER_ERROR); # Several parts of the protocol don't have much positive feedback. # The bonescheck and upload commands give back a 200 response with no # special headers on success. As a perhaps paranoid guard against # talking to something which isn't the server, I've asked Alexis to # add HEADER_HEARSE to all the server's responses. if (!defined $resp->header(HEADER_HEARSE)) { xdie "server response lacks ", HEADER_HEARSE, " header\n"; } elsif ($update ne '') { xdie "server said to update client ($update)\n", $resp->content; } elsif ($error eq F_ERROR_INFO) { (my $s = $resp->content) =~ s/^INFO: //; $s =~ /\S/ or xdie "server sent empty info message\n"; $s =~ s/\n+\z//; info $s; return 1, $resp; } elsif ($error ne '') { xwarn "server sent invalid ", HEADER_ERROR, " `$error'\n" if $error ne F_ERROR_FATAL; xdie "server sent error:\n", $resp->content; } return 0, $resp; } # Return true if TOKEN looks like a valid user token. sub valid_user_token { my ($token) = @_; return defined $token && length $token && $token !~ /\s/; } # Read the user token from $User_token_file and return it if it's valid. sub read_user_token_from_file { local *FILE; if (!open FILE, $User_token_file) { $! == ENOENT or xdie "$User_token_file exists, but can't be read:"; return; } my $token = ; chomp $token if defined $token; close FILE or xdie "error closing $User_token_file:"; valid_user_token $token or xdie "$User_token_file doesn't contain a valid token,", " remove it and try again\n"; return $token; } # Request a new user token from the server and return it. sub request_new_user_token { if (!$User_email) { xdie "you don't have a user token yet,", 'run "man hearse" for instructions'; } info "requesting new user account for $User_email"; my $req = make_req SERVER_CMD_NEW_USER; $req->header(HEADER_TOKEN, $User_email); my ($had_error, $resp) = server_cmd $req; my $token = clean_header $resp->header(HEADER_TOKEN); if (!defined $token) { xdie "successful response from server, but no token\n"; } if (!valid_user_token $token) { xdie "invalid token returned by server ($token)\n"; } local *FILE; mkpath dirname $User_token_file; my $old_mask = umask 077; if (!open FILE, ">$User_token_file" or !print FILE "$token\n" or !close FILE) { xdie "can't write to $User_token_file:"; } umask $old_mask; return $token; } # Get $User_token filled in any way possible. sub get_user_token { $User_token = read_user_token_from_file if !defined $User_token; $User_token = request_new_user_token if !defined $User_token; debug "user token = [$User_token]"; } # Get a lock on the lock file or die. (There's no unlock step, that # happens at process exit.) sub get_lock { my $file = $Lock_file; $file = $User_token_file if !defined $file; debug "lock file [$file]"; return if !defined $file || $file eq ''; open LOCK, "+<$file" or xdie "can't open $file read/write:"; my $attempt = 0; while (1) { $attempt++; flock LOCK, LOCK_EX | LOCK_NB and return; info "waiting to lock $file" if $attempt == 1; last if $attempt == $Lock_tries; debug "sleep $Lock_sleep"; sleep $Lock_sleep; } xdie "couldn't lock $file:"; } # Return the time that the last bones upload was done. sub last_upload_time { my @stat = stat $Last_run_stamp_file; my $t = @stat ? $stat[ST_MTIME] : 0; debug "last run ", scalar localtime $t; return $t; } # Set the last run time to now. sub update_last_upload_time { local *FILE; debug "touch $Last_run_stamp_file"; if (!open FILE, ">$Last_run_stamp_file" or !close FILE) { xdie "error writing to $Last_run_stamp_file:"; } } # Return true if BONES is a valid uncompressed bones file name. sub valid_bones_file_name { my ($file) = @_; # bon<0 | role code>. # XXX case_tolerant if appropriate return $file =~ /^bon[A-Z](0|$Role_re)\.([A-Z]|\d+)\z/; } # Given a PATH, return the file name to use with the server and an # open() arg used to read it. This handles both removing directories # from the path and undoing compression. The open() might be on a # pipe, so you can't expect to seek in the resulting filehandle. sub crack_bones_file_name { my ($path) = @_; my $compressed = basename $path; # A bones file might be called bonG0.Z, so I have to avoid stripping # the .Z (which could also be compression). Only try for uncompression # if the file name isn't already valid. my $uncompressed = valid_bones_file_name($compressed) ? $compressed : basename $compressed, @Compress_suffix; return $uncompressed, ($compressed eq $uncompressed) ? $path : "$Decompress_cmd \Q$path\E|"; } # Return true if a file named BONES (possibly compressed) exists locally. sub bones_exists_locally { my ($bones_name) = @_; for my $ext ('', @Compress_suffix) { return 1 if stat "$Bones_dir/$bones_name$ext"; } return 0; } # Load a bones file. Return a list: reference to bones data, CRC, # VERSION_1/2/3/4. sub load_bones { my ($bones_name, $open_spec) = @_; my ($data, $md5, @version); local *FILE; if (!open FILE, $open_spec) { xwarn "can't read $open_spec:"; return; } binmode FILE; $data = do { local $/; }; if (!close FILE) { xwarn "error reading $open_spec: ", ${!} ? "$!\n" : "exit status $?\n"; return; } # The 4 version numbers are stored by Nethack as 4 unsigned longs # in host byte order at the start of the file. I don't want to # read them in host order, though, because that would mask byte sex # differences between platforms. # # If the platform's longs aren't 4 bytes, though, I've got a # separate problem. I need to read the right number of bytes # else I'll only get part of the version data, and what I do get # will end up in the wrong places. I test for this using Perl # 5.6's 'L!' pack format (and just hope for the best for earlier # versions). I haven't actually written the code to deal with # this case yet (it needs special handling because there's no # format to read a native-sized long but with a specific byte # order), I just detect it and choke. my $ulong_size = eval { length pack 'L!', 0 } || 4; # punt for Perl < 5.6 if ($ulong_size != 4) { xdie "size of unsigned long is $ulong_size rather than 4\n"; } # struct version_info { # unsigned long incarnation; /* actual version number */ # unsigned long feature_set; /* bitmask of config settings */ # unsigned long entity_count; /* # of monsters and objects */ # unsigned long struct_sizes; /* size of key structs */ # }; @version = unpack 'V' x HEADER_VERSION_COUNT, $data; if (@version != HEADER_VERSION_COUNT) { xwarn "$open_spec is too short (", length($data), ")\n"; return; } $md5 = md5 \$data; debug "file $bones_name"; debug " size ", length $data; debug " version $_ $version[$_]" for 0..$#version; debug " md5 $md5"; return \$data, $md5, @version; } # Add the headers describing a bones file to an HTTP::Request. sub add_bones_info_to_req { my ($req, $bones_name, $md5, @version) = @_; $req->header(HEADER_FILE_NAME, $bones_name); $req->header(HEADER_BONES_CRC, $md5); for (1..HEADER_VERSION_COUNT) { $req->header(HEADER_VERSION . $_, $version[$_ - 1]); } } # Try to upload the given bones file to the server. Return true if it # was accepted. sub upload_one_bones { my ($bones_name, $open_spec) = @_; local *FILE; info "$bones_name offering to server"; debug "open_spec = [$open_spec]"; my ($rdata, @bones_info) = load_bones $bones_name, $open_spec or return; unshift @bones_info, $bones_name; my $req = make_req SERVER_CMD_BONES_CHECK; add_bones_info_to_req $req, @bones_info; # Send the info about this bones file to the server. my ($had_error, $resp) = server_cmd $req; # If the server doesn't want this file it returns an informational # error, which was printed by server_cmd. return if $had_error; # Upload the file. $req = make_req SERVER_CMD_UPLOAD; $req->header('Content-Type', 'application/octet-stream'); $req->header('Content-Transfer-Encoding', 'binary'); add_bones_info_to_req $req, @bones_info; $req->content($$rdata); ($had_error, $resp) = server_cmd $req; info $had_error ? "$bones_name upload failed" : "$bones_name uploaded"; return !$had_error; } # Upload all the new bones files. Return 2 booleans: Whether any were # accepted (and so I should try to download), and whether the last # upload timestamp should be updated. sub upload_new_bones { my ($since) = @_; my $any_tried = 0; my $any_uploaded = 0; local *DIR; opendir DIR, $Bones_dir or xdie "can't opendir $Bones_dir:"; while (defined(my $file = readdir DIR)) { my $path = "$Bones_dir/$file"; my ($bones_name, $open_spec) = crack_bones_file_name $path; # Skip non-bones files. next unless valid_bones_file_name $bones_name; # Keep track of the bones files present on this machine, the # server will want to know when the time comes to download. push @Local_bones, $bones_name; # Skip files created before the last time I uploaded. my @stat = stat $path; if (!@stat) { xwarn "can't stat $path:"; next; } next unless $stat[ST_MTIME] > $since; info 'uploading bones' unless $any_tried; $any_tried = 1; if (upload_one_bones $bones_name, $open_spec) { $any_uploaded = 1; if ($Delete_uploaded) { if (unlink $path) { info "$bones_name deleted"; $bones_name eq pop @Local_bones or die; } else { xwarn "error deleting $path:" } } } } closedir DIR or xdie "error closing directory $Bones_dir:"; info "no bones to upload" if !$any_tried && !$Quiet_cron; # The second returned boolean tells whether the last upload timestamp # should be updated. My strategy is to update it any time I offered # bones to the server and didn't get a fatal error, even if the server # didn't accept the bones. # # The Windows implementation only updates the timestamp if the # server accepts at least one bones file. There's a common case # for which this doesn't work when bones files are compressed: If # you use "nethack -D" to check out a bones file without unlinking # it, nethack will uncompress then recompress it, thereby updating # it. The next time I run I'll see it as new, and so I'll offer # it to the server. The server rejects it since it already has # that one. If there are no other local bones, I'll exit without # updating the timestamp, so the same thing will happen again on # the next run. # # To avoid this I update the timestamp whenever I offer any bones to # the server and I don't get a fatal error. (I could update it even # when I don't offer any bones to the server, I don't just to avoid # the useless updates to the timestamp file for when I'm being run # often.) return $any_uploaded, $any_tried; } # Write a bones file called BONES using the given data. sub write_bones { my ($bones_name, $rdata) = @_; local *FILE; my $already_have = "tried to write $bones_name," . " but we already have a bones file with that name\n"; # Compression and Nethack's lack of locking make it impossible to do # this without races. At least give it a shot. if (bones_exists_locally $bones_name) { xwarn $already_have; return; } my $tmp_path = "$Bones_dir/tmp.$Me.$bones_name"; my $real_path = "$Bones_dir/$bones_name"; my $unlink = sub { unlink $tmp_path or xwarn "error unlinking $tmp_path:"; }; my $old_mask = umask 0; if (!sysopen FILE, $tmp_path, O_WRONLY | O_CREAT, $Bones_mode) { xwarn "can't write to $tmp_path:"; umask $old_mask; return; } umask $old_mask; binmode FILE; if (!print FILE $$rdata or !close FILE) { xwarn "error writing to $tmp_path:"; $unlink->(); return; } # When Nethack creates a bones file, it checks whether the file # exists before starting, then does a rename() at the end. It # doesn't care about the race (somebody might have created one # after the initial test), there's no locking at all. I do care # about the race, though -- if somebody else was playing on the # system and they created a bones file in the interim, I'd rather # leave them the one they created than one I downloaded. # First, try linking the temporary name to the final name. This # fails if the final name already exists. if (!link $tmp_path, $real_path) { xwarn $already_have; $unlink->(); return; } # Drop the temporary name. $unlink->(); # A bones file with this name might have been created and compressed # in the interim. There's no safe way to deal with that (if I try # to erase mine, there's a separate race if another game tries to # used the compressed one (it overwrites mine)). Having the # compressed one be used in preference to mine is the best course # anyway, so don't do anything special to try to detect a compressed # one at this point. return 1; } # Try to download one bones file. Return true if successful. sub download_one_bones { my $req = make_req SERVER_CMD_DOWNLOAD; $req->header(HEADER_USER_LEVELS, # The server chokes if you don't have any local # levels, but this can happen if you use # --delete-uploaded or --force-download. Fill in a # bogus value to appease it. join ',', @Local_bones ? @Local_bones : 'himom'); my ($had_error, $resp) = server_cmd $req; return if $had_error; my $bones_name = clean_header $resp->header(HEADER_FILE_NAME); if (!defined $bones_name) { xdie "server didn't specify bones file name\n"; } elsif (!valid_bones_file_name $bones_name) { xdie "server specified invalid bones file name `$bones_name'\n"; } my $her_md5 = clean_header $resp->header(HEADER_BONES_CRC); if (!defined $her_md5) { xdie "server didn't specify bones CRC\n"; } my $my_md5 = md5 \$resp->content; if (lc $her_md5 ne lc $my_md5) { xdie "downloaded file $bones_name has CRC mismatch,", " hers $her_md5 mine $my_md5\n"; } # write_bones() tells me whether it was successful or not, but I # don't want to behave differently if it wasn't. The server might # have sent me a bones which wasn't in @Local_bones but which was # created here since. if (write_bones $bones_name, $resp->content_ref) { info "downloaded $bones_name"; push @Local_bones, $bones_name; } return 1; } # Download as many bones files as the server will give us. sub download_all_bones { info 'downloading bones'; 1 while download_one_bones; } sub main { init; @ARGV and usage "unknown non-switch args: @ARGV\n"; get_user_token; get_lock; # Wait until after get_user_token and get_lock to drop privs, so I # can write to the directory containing $User_token_file, and the # lock file. set_ugid; my $since = last_upload_time; my ($uploaded, $do_update) = upload_new_bones $since; # The protocol says you only ask the server for bones if you uploaded # any. Normally this is the only time any will be given to you anyway. # It can also happen when a new variant is opened up and there isn't a # reserve of bones built up yet. The server remembers how many it owes # you in this case, but you'll only get them the next time you upload a # bones file. download_all_bones if $uploaded || $Force_download; # Wait until after downloading to update the timestamp as an easy # way to avoid trying to upload something I downloaded. XXX An # unfortunate consequence is that a bones created locally while I'm # downloading will never be uploaded. update_last_upload_time if $do_update; return 0; } $Exit = main || $Exit; $Exit = 1 if $Exit && !($Exit % 256); exit $Exit; __END__ =head1 NAME hearse - exchange Nethack bones files with other players =head1 SYNOPSIS B [B<-b> | B<--bones-dir> I] [B<--bones-mode> I] [B<-c> | B<--config-file> I] [B<--cron>] [B<--debug>] [B<--delete-uploaded>] [B<--help>] [B<--lock-file> I] [B<-q> | B<--quiet>] [B<--run-as-me>] [B<--run-as-user> I] [B<--run-as-group> I] [B<--server-url> I] [B<--stamp-file> I] [B<--user-email> I
] [B<--user-token> I] [B<--user-token-file> I] [B<--version>] =head1 DESCRIPTION Nethack sometimes saves the level on which you die (including your stuff, what killed you, and your ghost) in a "bones file". These files get loaded into later Nethack games. If you're the only Nethack player on your system you'll only get bones files you created yourself. B lets you automatically exchange Nethack bones file with other players. When run it uploads any new bones files it finds on your system, then downloads any bones files the server feels like giving it. See L for more information. An important thing to note is that by default using B will cause you to end up with more bones than you otherwise would have. This changes the game's balance and is considered by many players to be a mild form of cheating. You can address this by turning on the B<--delete-uploaded> option, but the down side is you'll never encounter your own bones files. In order to use the Hearse server, you've got to supply your email address. Do this by using the B<--user-email> switch the first time you use the program, or by putting C> in the config file. Your email address will only be used to contact you about Hearse, and will never be given to any third party. If you enter an invalid address, the server won't be able to support you if you download a bad bones file, and will be forced to ban you if any of your uploaded files are bad. Hearse was set up as a service to the Nethack community. Please respect that; abuse of the service can only lead to it being removed. =head1 QUICK START The defaults are set up for a Linux system using a nethack binary which is either set-uid or set-gid games. If this is what you've got, as root run # hearse --user-email your@address.com one time by hand, then put 0 3 * * * root perl -we 'sleep rand 3600'; hearse --quiet in F. =head1 CONFIGURATION B comes with default values for its various configuration settings which match the way many Linux systems are set up. If any of them don't match your system, you can either change them in a configuration file, or you can specify the right values via command line switches. This last isn't as onerous as it sounds, because most people run it from cron. You can put the switches in the crontab file and leave it at that. If you'd rather use a configuration file, you can use the default location (F), or use the B<-c> (aka B<--config-file>) switch to specify the file you'd like to use. The configuration file can specify all of the options for which it makes sense, using the long version of the option name followed by the value. Blank and commented lines are ignored in the usual fashion. A string value can be given as C<-> to mean the empty string. Booleans can use on/off/true/false/yes/no/1/0. A sample F is included with the distribution. Eg, bones-dir /local/games/nethackdir bones-mode 600 quiet on run-as-user daemon run-as-group - user-token-file /local/games/nethackdir/hearse.user-token =head1 PRIVILEGES B needs to run with permissions like those used by Nethack itself, so that it can read and write the bones files. It should not be made set-uid or set-gid, though; it hasn't been audited for that. The default configuration will try to set both the user and group ids to C. Nethack itself will generally only be set-id to either one or the other, but using both hurts nothing and allows B to run as-is on more systems. This will only work if you run B as root. If you want to disable B's id setting and take care of it externally you can use the B<--run-as-me> switch to turn it off, or the B<--run-as-user> and B<--run-as-group> switches for finer grained control. Specify C<''> or C<-> for either of the latter to disable just that thing. =head1 RUNNING FROM CRON If you're using the pre-packaged F<.deb> or F<.rpm> version of B, the program is already set up to run automatically (both daily and when you connect to the Internet). You don't have to do anything unless you want to change this behavior. If you're installing B by hand, read on. The normal way to use the program is to run it from cron, either daily or on whatever schedule you like. (There's no harm in running it often, if it doesn't find any new bones files it doesn't even contact the server.) If letting it manage its own permissions, you'd just run it as root. Eg, to run it some time in the 3:00 hour, put something like 0 3 * * * root perl -we 'sleep rand 3600'; hearse --quiet in F. The randomization is to prevent the server from getting hammered at the top of each time zone's 3:00 hour. If you'd like to see what the server's doing, you can use B<--cron> rather than B<--quiet>. This will cause it to output its status message, but only when it actually transfers a bones file. =head1 RUNNING FOR MULTIPLE NETHACK VARIANTS If you use multiple Nethack variants which are supported by the Hearse server, you can run B for all of them. The normal way to do this is to run B once for each variant, specifying the bones directory on the command line # hearse -b /var/games/slashem leaving the rest of the configuration settings to be read from the configuration file. The last upload time is by default stored in the bones directory, so everything just works. The Hearse protocol requires that you have only a single concurrent connection for each user account (it decides what kind of bones file to send you based on the kind you most recently uploaded), so B does locking on the user token file in order to ensure this. See the B<--lock-file> switch for more info. =head1 OPTIONS =over 4 =item B<-b>, B<--bones-dir> I Specify the bones directory. By default the program uses the first of F, F, and the current directory which contains a file called F. =item B<--bones-mode> I Specify the mode for the bones files B creates. The default is 660. =item B<-c>, B<--config-file> I Specify an alternative configuration file. The default is F. =item B<--cron> Suppress the "no bones to upload" message. This makes it so that there's no output at all when there's nothing to do, but you still see what's going on when bones files are transfered. This is a nice way to run it from cron if you want to keep an eye on it. =item B<--debug> Turn debugging on. =item B<--delete-uploaded> Delete locally generated bones files after uploading them. Some people might want to do this in order to avoid changing the game's balance. Since the server normally gives you 1 bones file for each one you upload, if you delete your local bones after uploading them you'll end up with the same number of bones you otherwise would have had, but they'll be somebody else's rather than your own. =item B<--help> Show the usage message and die. =item B<--lock-file> I The Hearse protocol requires that B do locking to be sure that only a single connection per user can happen at a time. It does this by locking the B<--user-token-file>. You should not generally change this, but if you have special requirements (that that file be read only, eg), you can override it with this switch. Use C<''> to disable locking (which I do not recommend). =item B<-q>, B<--quiet> Don't output information messages. =item B<--run-as-me> Turn off both B<--run-as-user> and B<--run-as-group>. =item B<--run-as-user> I Use I as the real and effecitve user id, default C. You've generally got to be root for this to work. =item B<--run-as-group> I Use I as the real and effecitve group id, default C. You've generally got to be root for this to work. =item B<--server-url> I Specify the URL for the server program. See the source or the B<--help> message for the default. =item B<--stamp-file> I B only tries to upload bones files which were created since the last time it sucessfully talked to the server. The modification time of the B<--stamp-file> (F<.hearse.timestamp> by default) tells it when that was. This path is taken relative to the B<--bones-dir> (unless it's absolute). =item B<--user-email> I
Specify your email address. You only have to do this the first time you run B. =item B<--user-token> I Specify your user token directly. You won't normally need to do this, as B requests the token from the server and stores it in the B<--user-token-file> for later retrieval. =item B<--user-token-file> I Specify the file used to store the user token, by default F. =item B<--version> Show the version number and exit. =back =head1 AVAILABILITY The code is licensed under the GNU GPL. Check L for updated versions. =head1 AUTHOR This Unix client was written by Roderick Schertler . The Hearse protocol, server, and Windows client were written by Alexis Manning . =cut hearse-1.5/hearse.conf0000664000175000000620000000177607503377317015717 0ustar roderickstaff00000000000000# /etc/nethack/hearse.conf # # $Id: hearse.conf,v 1.5 2002/06/17 15:22:55 roderick Exp $ # # This is the configuration file for hearse, a program which exchanges # Nethack bones files with other players via a server on the Internet. # See the hearse(6) man page and http://www.argon.org/~roderick/hearse/ # for more information. # You can set the values for just about all the command line switches here, # using the long version of the option name. The default values are shown # here commented out. A string value can be given as - to mean the empty # string. Booleans can be specified with on/off/true/false/yes/no/1/0. # bones-dir # bones-mode 660 # cron off # debug off # delete-uploaded off # lock-file # quiet off # run-as-me off # run-as-group games # run-as-user games # server-url http://hearse.krollmark.com/bones.dll # stamp-file .hearse.timestamp # user-email - # user-token - # user-token-file /etc/nethack/hearse.user-token hearse-1.5/hearse.spec0000664000175000000620000000472710215047643015711 0ustar roderickstaff00000000000000# $Id: hearse.spec,v 1.7 2005/03/13 14:35:15 roderick Exp $ %define name hearse %define version 1.5 %define release 1 %define prefix %{_prefix} %define confdir /etc Summary: exchange Nethack bones files with other players Name: %{name} Version: %{version} Release: %{release} Prefixes: %{prefix} %{confdir} Copyright: GPL Group: Amusements/Games Source: http://www.argon.org/~roderick/hearse/%{name}-%{version}.tar.gz URL: http://www.argon.org/~roderick/hearse/ Packager: Roderick Schertler BuildArch: noarch BuildRoot: %{_builddir}/%{name}-root # I can actually use either Digest::MD5 or MD5, but I can't see how to # tell rpm that. # # I'd tried to set this requirement using $RPM_requires inside the # script, which /usr/lib/rpm/perl.req claims to support. They must not # have tested that feature at all, because a typo prevents it from # working. Requires: perl(Digest::MD5) %description Nethack sometimes saves the level on which you die (including your stuff, what killed you, and your ghost) in a "bones file". These files get loaded into later Nethack games. If you're the only Nethack player on your system you'll only get bones files you created yourself. With Hearse, you can automatically exchange bones files with other Nethack players. When run it uploads any new bones files it finds on your system, then downloads any bones files the server feels like giving it. See http://www.argon.org/~roderick/hearse/ for more information. %prep %setup %build perl Makefile.PL \ LIB=$RPM_BUILD_ROOT/none/such \ INSTALLMAN1DIR=$RPM_BUILD_ROOT%{prefix}/share/man/man1 \ INSTALLMAN3DIR=$RPM_BUILD_ROOT%{prefix}/share/man/man3 \ INSTALLBIN=$RPM_BUILD_ROOT%{prefix}/bin \ INSTALLSCRIPT=$RPM_BUILD_ROOT%{prefix}/bin make %install rm -rf $RPM_BUILD_ROOT mkdir -p $RPM_BUILD_ROOT make pure_perl_install mkdir -p $RPM_BUILD_ROOT%{confdir}/cron.d mkdir -p $RPM_BUILD_ROOT%{confdir}/cron.daily mkdir -p $RPM_BUILD_ROOT%{confdir}/nethack cp debian/hearse.cron.d $RPM_BUILD_ROOT%{confdir}/cron.d/hearse cp debian/hearse.cron.daily $RPM_BUILD_ROOT%{confdir}/cron.daily/hearse cp hearse.conf $RPM_BUILD_ROOT%{confdir}/nethack %clean rm -rf $RPM_BUILD_ROOT %files %defattr(-,root,root) %doc Notes %doc README* %doc debian/changelog %config %{confdir}/cron.d/hearse %config %{confdir}/cron.daily/hearse %config %{confdir}/nethack/hearse.conf %{prefix}/bin/bones-info %{prefix}/bin/hearse %{prefix}/share/man/man1/bones-info.1.gz %{prefix}/share/man/man1/hearse.1.gz hearse-1.5/debian/0002775000175000000620000000000010215047704014776 5ustar roderickstaff00000000000000hearse-1.5/debian/po/0002775000175000000620000000000010215047704015414 5ustar roderickstaff00000000000000hearse-1.5/debian/po/.cvsignore0000664000175000000620000000001610215045607017407 0ustar roderickstaff00000000000000templates.pot hearse-1.5/debian/po/POTFILES.in0000664000175000000620000000005310215045607017165 0ustar roderickstaff00000000000000[type: gettext/rfc822deb] hearse.templates hearse-1.5/debian/po/templates.pot0000664000175000000620000000341210215047704020134 0ustar roderickstaff00000000000000# # Translators, if you are not familiar with the PO format, gettext # documentation is worth reading, especially sections dedicated to # this format, e.g. by running: # info -n '(gettext)PO Files' # info -n '(gettext)Header Entry' # # Some information specific to po-debconf are available at # /usr/share/doc/po-debconf/README-trans # or http://www.debian.org/intl/l10n/po-debconf/README-trans # # Developers do not need to manually edit POT or PO files. # #, fuzzy msgid "" msgstr "" "Project-Id-Version: PACKAGE VERSION\n" "Report-Msgid-Bugs-To: \n" "POT-Creation-Date: 2005-03-13 09:35-0500\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: FULL NAME \n" "Language-Team: LANGUAGE \n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=CHARSET\n" "Content-Transfer-Encoding: 8bit\n" #. Type: string #. Description #: ../hearse.templates:3 msgid "Email address to submit to the Hearse server:" msgstr "" #. Type: string #. Description #: ../hearse.templates:3 msgid "" "The Hearse server requires that you supply an email address before you can " "exchange bones files. If you supply an address here it will be submitted to " "the server for you. If you don't supply an address, hearse will be " "installed but it won't run automatically until you create an account " "yourself." msgstr "" #. Type: string #. Description #: ../hearse.templates:3 msgid "" "The server operator states that your email address will only be used to " "contact you about Hearse, and will never be given to any third party. If " "you enter an invalid address, the server won't be able to support you if you " "download a bad bones file, and will be forced to ban you if any of your " "uploaded files are bad." msgstr "" hearse-1.5/debian/changelog0000664000175000000620000000725210215047645016660 0ustar roderickstaff00000000000000hearse (1.5) unstable; urgency=medium * Note (in the README, debian/control, the man page, and the web page) that running without --delete-uploaded changes the game's balance (closes: #279264). * Switch to gettext-based debconf templates (closes: #250268, thanks to Martin Quinson). * Update Alexis' email address. * Tweak debian/watch RE. -- Roderick Schertler Sun, 13 Mar 2005 09:26:09 -0500 hearse (1.4) unstable; urgency=high * Deal with Getopt::Long changes in Perl 5.8.1 (closes: #214111). * Use the new dh_installppp instead of doing it by hand (closes: #212901). -- Roderick Schertler Tue, 21 Oct 2003 15:33:45 -0400 hearse (1.3) unstable; urgency=low * Oops, binmode() the bones being written out. This is required both for non-Unix, and for Perl 5.8 with a UTF8 locale. * Choke if the bones received from the server doesn't have a CRC to check against. * Clarify that the .deb and .rpm versions already run from cron (closes: #197205). * Add the current directory to the list of possible bones directories (for the benefit of Windows). Consequently validate a bones directory by checking if it has a "record" file in it. -- Roderick Schertler Wed, 30 Jul 2003 15:44:47 -0400 hearse (1.2) unstable; urgency=low * bones-info: Correct documentation for --big-endian and --little-endian. -- Roderick Schertler Sat, 2 Nov 2002 10:12:27 -0500 hearse (1.1) unstable; urgency=low * bones-info: Add --verbose switch. Thanks to Malcolm Ryan for the initial implementation. * bones-info: Add --little-endian, --big-endian, and --auto switches. * Depend on nethack, otherwise the package install fails at postinst time because hearse can't find a bones directory (closes: #158912). -- Roderick Schertler Fri, 1 Nov 2002 17:03:22 -0500 hearse (1.0-1) unstable; urgency=low * Oops, it's /etc/ppp/ip-up.d/hearse, not /etc/ppp/ip-up.d/hearse.ip-up (closes: #150983). -- Roderick Schertler Wed, 26 Jun 2002 14:16:19 -0400 hearse (1.0) unstable; urgency=low * Add --delete-uploaded option. * Beef up description (closes: #149865). -- Roderick Schertler Mon, 17 Jun 2002 11:23:59 -0400 hearse (0.4) unstable; urgency=low * Update the timestamp if you have any bones to offer and can successfully talk to the server, even if the server rejects the bones. * Upload to Debian (closes: #148395). * Create RPM package. * Add locking. * Add stand-alone bones-info program. * Oops, enable handling of $http_proxy and such. * Turn on LWP debugging when given --debug. * Install /etc/ppp/ip-up.d/hearse, to work with machines which aren't always on the network. * Run from cron.daily rather than cron.d, to work with machines which aren't left on all the time. -- Roderick Schertler Sun, 26 May 2002 19:30:58 -0400 hearse (0.3) unstable; urgency=low * Oops, process the config file if no args were specified. * Allow libmd5-perl to satisfy the Debian package's dependency. -- Roderick Schertler Fri, 24 May 2002 13:33:53 -0400 hearse (0.2-1) unstable; urgency=low * Tweak the message used if there's no user token. * Correct the markup used for email addresses. * s/Recomments/Recommends/ in control file. -- Roderick Schertler Wed, 22 May 2002 19:22:16 -0400 hearse (0.1-1) unstable; urgency=low * Initial version. -- Roderick Schertler Wed, 22 May 2002 17:10:17 -0400 $Id: changelog,v 1.17 2005/03/13 14:35:17 roderick Exp $ hearse-1.5/debian/control0000664000175000000620000000246710215045605016406 0ustar roderickstaff00000000000000Source: hearse Section: games Priority: optional Maintainer: Roderick Schertler Standards-Version: 3.5.6 Build-Depends: debhelper (>= 4.1.68), perl (>= 5.6.0-16) Package: hearse Architecture: all Depends: debconf (>= 1.2.0), ${perl:Depends}, libdigest-md5-perl | libmd5-perl, libwww-perl, nethack-common | nethack Description: exchange Nethack bones files with other players Nethack sometimes saves the level on which you die (including your stuff, what killed you, and your ghost) in a "bones file". These files get loaded into later Nethack games. If you're the only Nethack player on your system you'll only get bones files you created yourself. . With Hearse, you can automatically exchange bones files with other Nethack players. When run it uploads any new bones files it finds on your system, then downloads any bones files the server feels like giving it. See http://www.argon.org/~roderick/hearse/ for more information. . An important thing to note is that by default using Hearse will cause you to end up with more bones than you otherwise would have. This changes the game's balance and is considered by many players to be a mild form of cheating. You can address this by turning on the --delete-uploaded option, but the down side is you'll never encounter your own bones files. hearse-1.5/debian/copyright0000664000175000000620000000150707473004326016737 0ustar roderickstaff00000000000000$Id: copyright,v 1.1 2002/05/22 21:08:38 roderick Exp $ Copyright (C) 2002 Roderick Schertler. This program 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 2 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. For a copy of the GNU General Public License write to the Free Software Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA On Debian systems, the complete text of the GNU General Public License can be found in `/usr/share/common-licenses/GPL'. hearse-1.5/debian/hearse.config0000775000175000000620000000032107473004326017436 0ustar roderickstaff00000000000000#!/bin/sh -e # $Id: hearse.config,v 1.1 2002/05/22 21:08:38 roderick Exp $ . /usr/share/debconf/confmodule [ -s /etc/nethack/hearse.user-token ] || { db_input high hearse/user-email || true db_go } hearse-1.5/debian/hearse.cron.d0000664000175000000620000000164307477027612017370 0ustar roderickstaff00000000000000# $Id: hearse.cron.d,v 1.3 2002/06/04 03:10:02 roderick Exp $ # # The standard Debian and RPM installations run hearse from /etc/cron.daily. # This allows it to work (via anacron) with machines which aren't left on # all the time. # # hearse itself works well if run frequently. If it doesn't find any # new bones files it doesn't even contact the server. # # To run it more often than once a day, you can uncomment the cron entry # here. You should also make a change at the top of /etc/cron.daily/hearse # as indicated by the comments there, else you'll get the occasional email # as both try to run at once and the locking kicks in. # If you'd like to see what the server's doing, you can use --cron rather # than --quiet. This will cause it to output its status messages, but # only when it actually transfers a bones file. #*/15 * * * * root test -f /usr/bin/hearse -a -s /etc/nethack/hearse.user-token && hearse --quiet hearse-1.5/debian/hearse.cron.daily0000775000175000000620000000204007477027622020243 0ustar roderickstaff00000000000000#!/bin/sh -e # # $Id: hearse.cron.daily,v 1.2 2002/06/04 03:10:10 roderick Exp $ # The standard Debian and RPM installations run hearse once a day. The # program itself works well when run more frequently (if it doesn't find # any new bones files it doesn't even contact the server). If you'd # like to do that you could uncommont the following "exit" command and # edit /etc/cron.d/hearse according to the comments in it. # #exit 0 # If you'd like to see what the server's doing, you can use --cron rather # than --quiet. This will cause it to output its status messages, but # only when it actually transfers a bones file. hearse_args=--quiet #hearse_args=--cron #hearse_args=--debug # Bail if the package has been removed but not purged. [ -f /usr/bin/hearse ] || exit 0 # Bail if there's no user token. [ -s /etc/nethack/hearse.user-token ] || exit 0 # Add a bit of randomization to help prevent the server from getting # hammered at the top of each time zone's cron.daily hour. ( perl -we 'sleep rand 1800' hearse $hearse_args ) & hearse-1.5/debian/hearse.dirs0000664000175000000620000000003407474764430017142 0ustar roderickstaff00000000000000etc/nethack etc/ppp/ip-up.d hearse-1.5/debian/hearse.postinst0000775000175000000620000000037407473004326020064 0ustar roderickstaff00000000000000#!/bin/sh -e # $Id: hearse.postinst,v 1.1 2002/05/22 21:08:38 roderick Exp $ . /usr/share/debconf/confmodule [ -s /etc/nethack/hearse.user-token ] || { db_get hearse/user-email [ -z "$RET" ] || hearse --user-email "$RET" } #DEBHELPER# hearse-1.5/debian/hearse.ppp.ip-up0000775000175000000620000000054707745306056020042 0ustar roderickstaff00000000000000#!/bin/sh -e # $Id: hearse.ppp.ip-up,v 1.1 2003/10/21 19:42:38 roderick Exp $ test -f /usr/bin/hearse -a -s /etc/nethack/hearse.user-token || exit # If you'd like to see what the server's doing, you can use --cron rather # than --quiet. This will cause it to output its status messages, but # only when it actually transfers a bones file. hearse --quiet hearse-1.5/debian/hearse.templates0000664000175000000620000000133410215045605020162 0ustar roderickstaff00000000000000Template: hearse/user-email Type: string _Description: Email address to submit to the Hearse server: The Hearse server requires that you supply an email address before you can exchange bones files. If you supply an address here it will be submitted to the server for you. If you don't supply an address, hearse will be installed but it won't run automatically until you create an account yourself. . The server operator states that your email address will only be used to contact you about Hearse, and will never be given to any third party. If you enter an invalid address, the server won't be able to support you if you download a bad bones file, and will be forced to ban you if any of your uploaded files are bad. hearse-1.5/debian/rules0000775000175000000620000000270310215045605016054 0ustar roderickstaff00000000000000#!/usr/bin/make -f # $Id: rules,v 1.5 2005/03/13 14:17:41 roderick Exp $ dt := debian/hearse prefix = `pwd`/$(dt) stamp_build := debian/stamp.build stamp_install := debian/stamp.install clean := $(stamp_build) $(stamp_install) ifneq "" "$(findstring debug,$(DEB_BUILD_OPTIONS))" CFLAGS += -g endif export DH_COMPAT := 3 PERL ?= perl build: $(stamp_build) $(stamp_build): dh_testdir $(PERL) Makefile.PL INSTALLDIRS=vendor $(MAKE) OPTIMIZE="-O2 -Wall $(CFLAGS)" $(MAKE) test touch $@ install: $(stamp_install) $(stamp_install): $(stamp_build) dh_testdir dh_testroot dh_clean -k dh_installdirs $(MAKE) install PREFIX=$(prefix)/usr cp hearse.conf $(dt)/etc/nethack find $(prefix) -depth -type d -print0 | \ xargs -0r rmdir --ignore-fail-on-non-empty touch $@ clean: dh_testdir dh_testroot [ ! -f Makefile ] || $(MAKE) realclean dh_clean $(clean) debconf-updatepo binary: binary-indep binary-arch binary-arch: binary-indep: $(stamp_install) dh_testdir dh_testroot dh_installdebconf dh_installdocs dh_installexamples dh_installmenu # dh_installlogrotate # dh_installemacsen # dh_installpam # dh_installmime # dh_installinit dh_installcron dh_installppp dh_installman dh_installinfo # dh_undocumented dh_installchangelogs dh_link dh_strip dh_compress dh_fixperms # dh_makeshlibs dh_installdeb dh_perl dh_shlibdeps dh_gencontrol -u -isp dh_md5sums dh_builddeb .PHONY: build install clean binary-indep binary-arch binary hearse-1.5/debian/watch0000664000175000000620000000020410215026453016017 0ustar roderickstaff00000000000000# $Id: watch,v 1.2 2005/03/13 12:08:11 roderick Exp $ version=2 http://www.argon.org/~roderick/hearse/dist/hearse-(\d.*)\.tar\.gz