Beefy Boxes and Bandwidth Generously Provided by pair Networks
good chemistry is complicated,
and a little bit messy -LW
 
PerlMonks  

Talking to Magnetic Tapes

by submersible_toaster (Chaplain)
on Oct 17, 2002 at 05:56 UTC ( [id://205936]=perlmeditation: print w/replies, xml ) Need Help??

Searching CPAN for a module to help me manipulate a magnetic tape device , I discovered Mt version 0.0.1 : ) , Which is a wrapper for mt in perl. This is something I used to do constantly in my previous job IE make perlscripts that were 'aware' of things like fsf,bsf,bot,eod and would do a pretty good job of positioning the tape for appends, even to the point of abstracting the tape further away from the program, keeping a running index as the last element on tape AKA CiTransfer (if you've had the _pleasure_ )

What really struck me as odd though is that the CPAN Mt has only two methods, a constructor and a ->command method, which is passed a hash that describes

( -command=>'fsf etc here' , -count=>'some number', -args=>@arrayref );

If that's a wrapper , then what have I written? an interface ?. My module uses methods like ;

$tape->forward(2); $tape->rewind; $tape->positionforWrite; $tape->checkindex $tape->writeindex(@index);

Is this any use to any bugger but me? Could one monk spout more rhetoric in a single meditation ? I would hope not. To be revisited.

Replies are listed 'Best First'.
Re: Talking to Magnetic Tapes
by Aristotle (Chancellor) on Oct 17, 2002 at 13:43 UTC
    Mt is a wrapper, yours is a wrapper. Yours is higher level than Mt - but they're both wrappers. I would think your module might be useful - depending on how robustly written it is and whether you can release work you did at or even for work, you might want to look into getting it onto CPAN.

    Makeshifts last the longest.

      Hehe... lets just say I am entirely culpable for this module, which will prolly need a better name that Datanoid::tapedevice
        How about Device::MagneticTape? There's a Device:: namespace on CPAN for purposes like this, I imagine this would fit in there perfectly.

        Makeshifts last the longest.

Re: Talking to Magnetic Tapes
by submersible_toaster (Chaplain) on Oct 22, 2002 at 08:24 UTC
    Inspired to clean up my code (slightly) here is a wrapper for mt. I'm eager for fellow monks to give me some feedback on this.
    #!/usr/bin/perl -w package Device::MagneticTape; use Data::Dumper; use strict; my %self; my %cmd; my %dc = ( fsf=>'fsf', bsf=>'bsf', eod=>'eod', unload=>'unload', fsr=>'fsr', bsr=>'bsr', status=>'status', exist=>'exist', rewind=>'rewind' ); my %statval = ( eof=>'EOF' , bot=>'BOT' , eot=>'EOT' , eod=>'EOD' , online=>'ONLINE' , dr_open=>'DR_OPEN' , busy=>'BUSY' , ); my %linux = ( unload=>'eject' ); my %irix = ( eod=>'feom' ); my $ostype = `uname`; chomp $ostype; %cmd = ( %dc , %linux ) if ($ostype =~ /linux/i); %cmd = ( %dc , %irix ) if ($ostype =~ /irix/i); sub new { my $self = shift; my $device = shift; my $mt = shift; my %tape = ( mt => $mt, device => $device, command => "$mt -f $device " ); # Sanity check the device and mt binary # if all is good then bless %tape and return, return bless {%tape} , $self; } # END contsructor #Methods; sub rewind { my $self = shift; my $command = $self->{command} . $cmd{rewind}; warn "rewind\n"; my $err; $self->stat; $err = system ( $command ) unless ($self->{busy}); return $err; } sub gotoend { my $self = shift; my $command = $self->{command} . $cmd{eod}; my $err; warn "gotoend\n"; $self->stat; $err = system ( $command ) unless ($self->{busy}); return $err; } sub goto { my $self = shift; my $val = shift; $self->rewind; return $self->forwardspace($val); } sub forwardspace { my $self = shift; my $distance = shift; my $err; $distance = 1 unless defined $distance; warn "forwardspace $distance\n"; my $command = join " " , $self->{command} , $cmd{fsf} , $distance; $self->stat; $err = system ( $command ) unless ($self->{busy}); return $err; } sub backwardspace { my $self = shift; my $distance = shift; my $err; $distance = 1 unless defined $distance; warn "backwardspace $distance\n"; my $command = join " " , $self->{command} , $cmd{bsf} , $distance; $self->stat; $err = system ( $command ) unless ($self->{busy}); return $err; } sub forwardblock { my $self = shift; my $blocks = shift; my $err; $blocks = 1 unless defined $blocks; warn "forwardblock $blocks\n"; my $command = join " " , $self->{command} , $cmd{fsr} , $blocks; $self->stat; $err = system ( $command ) unless ($self->{busy}); return $err; } sub backwardblock { my $self = shift; my $blocks = shift; my $err; $blocks = 1 unless defined $blocks; warn "backwardblock $blocks\n"; my $command = join " " , $self->{command} , $cmd{bsr} , $blocks; $self->stat; $err = system ( $command ) unless ($self->{busy}); return $err; } sub endofdata { my $self = shift; my $err; warn "endofdata\n"; my $command = join " " , $self->{command} , $cmd{eod}; $self->stat; $err = system ( $command ) unless ($self->{busy}); return $err; } sub stat { my $self = shift; my $command = join " " , $self->{command} , $cmd{status}; warn "stat\n"; my @stats = `$command`; # Blank the hash map {$self{$_}=0} keys %statval; foreach (@stats) { $self->{busy} = 1 if ($_ =~ /resource busy/i); $self->{bot}=1 if ( $_ =~ / bot /i ); $self->{bot}=0 if ( $_ =~ /not at bot/i ); $self->{eod}=1 if ( $_ =~ / eod /i ); $self->{eof}=1 if ( $_ =~ / eof /i ); $self->{eof}=1 if ( $_ =~ /at fmk /i ); $self->{wr_prot}=1 if ( $_ =~ / wr_prot /i ); $self->{online}=1 if ( $_ =~ / online /i ); } print "\nStatComplete\n"; } # MetaMethods sub new_media { my $self = shift; my $name = shift; chomp $name; $self->rewind; open ( INDEX , '>>' , $self->{device} ) || die "Cant make newmedia $! +"; print INDEX "$name\n"; close INDEX; } sub writeindex { my $self = shift; my @index = @_; chomp(@index); $self->gotoend; unshift (@index , $self->{mastername}); print "Writing Index\n"; open ( INDEX , '>>' , $self->{device} ) || die "Cant write index $!"; print INDEX join "\n" , @index; close INDEX; } sub getindex { my $self = shift; my @index; $self->positionforwrite; open( INDEX , $self->{device} ) || die "Can't open index from device" + ; @index = <INDEX>; close INDEX; my $name = shift @index; chomp($name); $self->{mastername} = $name ; print "Media Label : ".$self->{mastername} . "\n"; my $i=0; #foreach (@index) { # print "$i : $_"; # ++$i; #} return @index; } sub positionforwrite { my $self = shift; $self->gotoend; $self->backwardspace(1); $self->backwardblock(1); } #writemyindex #getmyindex # Data Accessors sub status { my $self = shift; my $var = shift; return "" unless defined $var; return $self->{$var}; } 1; =head1 NAME Device::MagneticTape =head1 SYNOPSIS my $tape = Device::MagneticTape->new( $device , $mtbin ); my @index = $tape->getindex; $tape->positionforwrite; my $target = '/something/to/archive' # write to tape using your favorite archiver push (@index , $target); $tape->writeindex(@index); The standard methods return the same exit status as mt, generally thi +s is - 0 for success. 1 for unrecognised commands. 2 if an operation +fails. A method will return an undef if it was unable to execute due +to the device being 'busy'. =head1 METHODS =over 1 =head2 new ( $device , $mtbin ) takes two arguments, the device to use (make sure you use NO-rewind), +and the path to your system's mt binary. Returns a new Device::Magnet +icTape object with the following methods. =head2 rewind Rewind the tape. erm.. =head2 gotoend Space forward on the tape until reaching the end of data (B<EOD>). =head2 goto ( $tapefile ) Goto the numeric tapefile from BOT. Remember the first tapefile is num +ber 0. =head2 forwardspace ( $distance ) Space forware $distance tapefiles. =head2 backwardspace ( $distance ) Space backward $distance tapefiles. B<Note: To position correctly for +a read you should also move forwardblock(1) after any backwardspace>. =head2 forwardblock ( $blocks ) Move forward on the tape any given number of $blocks. =head2 backwardblock ( $blocks ) Move backward on the tape any given number of $blocks. =head2 endofdata See B<gotoend> above. =head2 stat Query the object's status as given by 'mt -f /dev/tape stat'. =back 1 =head1 META-METHODS In addition to supporting basic tape transport, the following methods +are provided. =over 1 =head2 new_media ( $name ) Create the first instance of an index on the current object, $name def +ines the media name. =head2 writeindex ( @index ) Accepts a list as it's argument , writeindex will update the tape inde +x to reflect @index. =head2 getindex Returns a list as it was stored by ->writeindex =head2 positionforwrite Positions the tape on the FMK B<before> the index is stored. When usin +g indexed tapes, ->positionforwrite B<before> appending to a tape. =back 1 =head1 BUGS Bound to be really, you should let me know if you find them <abramble@ +bigpond.net.au>. I would think this POD still needs work. =head1 AUTHOR Andrew Bramble <abramble@bigpond.net.au> =head1 SEE ALSO mt(1) , st(4) =head1 COPYRIGHT Copyright 2002 Andrew Bramble <abramble@bigpond.net.au> Now hear this. Distribute, repair, reuse, recycle, modify as much as y +ou like or can - it's free. =cut

      I assume you left the various warns in there just for development? The OS detection could be abstracted a bit further; some user control over it might not be a bad idea either. I'd also prefer if the commands produced Perlish boolean results, with an extra method to ask for the error code.

      I'd pull together the declaration of variables with their initial assignment in many places, though one might argue that's just preference. You could use the boolean results of matches in your stat method. And a map in void context?! :-)

      Some more abstraction would help. Note how your command routines all look almost the same. system LIST is usually better than system EXPR. Also, note how many of your routines call other command routines without checking intermediate results - not necessarily a good idea.

      The hardest to refactor was your stat routine (status in my version; your status is called get_status there).

      The following is untested due to lack of a magnetic tape. :-) It does compile properly at least. I left a couple of notes in a few places where I wasn't sure whether what you really meant.

      #!/usr/bin/perl -w package Device::MagneticTape; use strict; use IPC::Open2; use vars qw($ERROR); my %tape_cmd = ( fsf => 'fsf', bsf => 'bsf', eod => 'eod', unload => 'unload', fsr => 'fsr', bsr => 'bsr', status => 'status', exist => 'exist', rewind => 'rewind', ); my %os_specific_cmd = ( "" => {}, # dummy entry linux => { unload =>'eject', }, irix => { eod => 'feom', }, ); my %status = ( busy => qr/\bresource busy\b/i, bot => qr/\bbot\b/i, notbot => qr/\bnot at bot\b/i, eod => qr/\beod\b/i, eof => qr/\beof\b|\bat fmk\b/i, wr_prot => qr/\bwr_prot\b/i, online => qr/\bonline\b/i, # eot => qr//i, # what are these? # dr_open => qr//i, ); sub _assert { my ($bool, $msg) = @_; $ERROR = $msg unless $bool; return $bool; } sub new { my ($class, %self) = shift; @self{qw(device mt os)} = @_; $self{os} ||= do { chomp(my $ostype = `uname`); grep $ostype =~ /$_/i, keys %os_specific_cmd; } || ""; # make sure it's defined _assert(exists $os_specific_cmd{$self{os}}, "Unknown OS type: $sel +f{os}") and _assert(-e $self{mt}, "No such binary: $self{mt}") and _assert(-x _, "No permission to execute binary: $self{mt}") and _assert(-e $self{device}, "No such device: $self{device}") and _assert(-r _, "No permission to read device: $self{device}") and _assert(-w _, "No permission to write device: $self{device}") or return; $self{qw(error cmd)} = (0, { %tape_cmd, %{$os_specific_cmd{$self{o +s}}} }); my $self = bless \%self, $class; $self->status; return $self; } sub error { # CLASS *AND* INSTANCE METHOD my $self = shift; my ($code, $err); $err = ref $self ? \$self->{error} : \$ERROR; ($code, $$err) = ($$err, 0); return $code; } sub get_status { my $self = shift; my $var = shift; return defined $var ? $self->{$var} : ""; } sub status { my $self = shift; open2( my ($rdh, $wrh), $self->{mt}, -f => $self->{device}, $self->{cmd}->{status} ); # begin deep magic my @status_flag = keys %status; my $rx = do { local $" = '|'; qr/@{[map "($_)", values %status]}/; }; $self->{$_} = 0 for @status_flag; my @check = do { local $/; local $_ = <$rdh>; /$rx/ }; $self->{$_} = 1 for map $status_flag[$_], grep defined $check[$_], 0 .. $#check; # end deep magic $self->{bot} = 0 if $self->{notbot}; } ################### meta methods sub new_media { my $self = shift; chomp(my $name = shift); { my $try_rw; ($try_rw = $self->rewind) or return $try_rw; } return unless open my $index , '>>' , $self->{device}; print $index "$name\n"; close $index; return 1; } sub write_index { my $self = shift; chomp(my @index = @_); unshift @index, $self->{mastername}; { my $try_ff; ($try_ff = $self->gotoend) or return $try_ff; } return unless open my $index, '>>' , $self->{device}; print $index join("\n" , @index); # are you sure it shouldn't be m +ap "$_\n"? close $index; return 1; } sub get_index { my $self = shift; $self->positionforwrite; return unless open my $index, "<", $self->{device}; chomp(my @index = <$index>); close $index; $self->{mastername} = shift @index; return @index; } sub position_for_write { my $self = shift; return $self->gotoend and $self->backwardspace(1) and $self->backwardblock(1); } sub goto { my $self = shift; my $pos = shift || return; # no point in goto if no position return $self->rewind() and $self->forwardspace($pos); } sub gotoend { my $self = shift; return $self->endofdata; } ################### workhorses sub execute { my $self = shift; $self->{error} = 0; $self->status; return if $self->{busy}; $self->{error} = system $self->{mt}, -f => $self->{device}, @_; return $self->{error} == 0; } sub endofdata { my $self = shift; return $self->execute($self->{cmd}->{eod}); } sub rewind { my $self = shift; return $self->execute($self->{cmd}->{rewind}); } sub forwardspace { my $self = shift; my $distance = shift || 1; return $self->execute($self->{cmd}->{fsf}, $distance); } sub backwardspace { my $self = shift; my $distance = shift || 1; return $self->execute($self->{cmd}->{bsf}, $distance); } sub forwardblock { my $self = shift; my $blocks = shift || 1; return $self->execute($self->{cmd}->{fsr}, $blocks); } sub backwardblock { my $self = shift; my $blocks = shift || 1; return $self->execute($self->{cmd}->{bsr}, $blocks); } 1;

      Of course the POD will need some minor updates. :-)

      Oh, you might possibly want to consider changing your copyright information - the standard boilerplate is "This module is free software; you can redistribute it and/or modify it under the same terms as Perl itself." If you don't want that, make sure your copyright legally really means the things you want it to mean.

      Makeshifts last the longest.

Re: Talking to Magnetic Tapes
by submersible_toaster (Chaplain) on Oct 23, 2002 at 04:43 UTC
    Thanks Aristotle for turning my module inside out. I'm still struggling to work it all out.
    I found that this line in the constructor.
    $self{qw(error cmd)} = (0, { %tape_cmd, %{$os_specific_cmd{$self{os}}} + });

    would create $self->{errorcmd} instead of setting error=>0 and cmd=>{hashstuff}
    So I put this one on two lines and it worked.

    $self{error}=0; $self{cmd}= { %tape_cmd , %{os_specific_cmd{$self{os}}} };

    One thing that has put me into a tailspin is your use of $" and $/ . As an interesting aside, I think there is a misprint in Perl In a Nutshell-2ndEdition , whilst the index points to page 55 for global variable $" , page 55 claims the list separator is either $ or $LIST_SEPARATOR, no mention of $" - hmmm.

    This will compile on perl5.6.1 , my poor Indy's/Octane's are still on 5.004 (something that will be remedied asap). I will read further into the behaviour of IPC::Open2 , I'm not certain yet but something is now breaking the get_index method. More on this as I investigate.

    I think OO and hashes and nested data are slowly beginning to make sense to me.

    Update : Have made some more modifications around some of the logic here, Aristotle wisely spoke of examining more closely the return values of each methodcall. I discovered that the lowerprecedence 'and' used in the position_for_write method was only calling one thing.

    return $self->endofdata and $self->backwardspace(1) and $self->backwardblock(1);
    Only ever executes 'endofdata' , whereas
    return $self->endofdata && $self->backwardspace(1) && $self->backwardblock(1);
    behaves as I would expect.
    Reading about 'or' vs '', '&&' vs 'and' didn't really explain this though.


    Update: More abstraction/less abstraction, I wonder about the needs for methodnames like backwardspace, forwardspace . Decided to catch-all the commands in %cmd via AUTOLOAD and execute them, applying this to Aristotle's code reduces to workhorses down to an execute method and the AUTOLOADer.

    #!/usr/bin/perl -w package Device::MagneticTape; $VERSION=0.1; use strict; use IPC::Open2; use Data::Dumper; use vars qw($ERROR $AUTOLOAD); my %tape_cmd = ( fsf => 'fsf', bsf => 'bsf', eod => 'eod', unload => 'unload', fsr => 'fsr', bsr => 'bsr', status => 'status', exist => 'exist', rewind => 'rewind', ); my %os_specific_cmd = ( "" => {}, # dummy entry linux => { unload =>'eject', }, irix => { eod => 'feom', }, ); my %status = ( busy => qr/\bresource busy\b/i, bot => qr/\bbot\b/i, notbot => qr/\bnot at bot\b/i, eod => qr/\beod\b/i, eof => qr/\beof\b|\bat fmk\b/i, wr_prot => qr/\bwr_prot\b/i, online => qr/\bonline\b/i, # eot => qr//i, # what are these? # dr_open => qr//i, ); sub _assert { my ($bool, $msg) = @_; #warn "Assert , $bool , $msg"; $ERROR = $msg unless $bool; return $bool; } sub new { my ($class, %self) = shift; @self{qw(device mt os)} = @_; $self{os} ||= do { chomp ( my $ostype = `uname` ); grep $ostype =~ /$_/i, keys %os_specific_cmd; # MODIFIED , $self{os} ||= was being set to 2. # presumably that is what grep is evaluating to. lc($ostype); } || ""; # make sure it's defined # MODIFIED the _assert block to return $ERROR. _assert(exists $os_specific_cmd{$self{os}}, "Unknown OS type: $sel +f{os}") and _assert(-e $self{mt}, "No such binary: $self{mt}") and _assert(-x _, "No permission to execute binary: $self{mt}") and _assert(-e $self{device}, "No such device: $self{device}") and _assert(-r _, "No permission to read device: $self{device}") and _assert(-w _, "No permission to write device: $self{device}") or return $ERROR; #$self{qw(error cmd)} = ( 0 , ( %tape_cmd, %{$os_specific_cmd{$se +lf{os}}} ) ); # MODIFIED $self{error}=0; $self{cmd} = { %tape_cmd, %{$os_specific_cmd{$self{os}}} } ; my $self = bless \%self, $class; $self->status; return $self; } sub error { # CLASS *AND* INSTANCE METHOD my $self = shift; my ($code, $err); $err = ref $self ? \$self->{error} : \$ERROR; ($code, $$err) = ($$err, 0); return $code; } sub get_status { my $self = shift; my $var = shift; return defined $var ? $self->{$var} : ""; } sub status { my $self = shift; open2( my ($rdh , $wdh) , $self->{mt}, -f => $self->{device}, $self->{cmd}->{status} ); # begin deep magic my @status_flag = keys %status; my $rx = do { local $" = '|'; qr/@{[map "($_)", values %status]}/; }; $self->{$_} = 0 for @status_flag; my @check = do { local $/; #local $_ = <$rdh>; local $_ = <$rdh>; /$rx/ }; $self->{$_} = 1 for map $status_flag[$_], grep defined $check[$_], 0 .. $#check; # end deep magic # MODIFIED #close $rdh; #close $wdh; $self->{bot} = 0 if $self->{notbot}; } ################### meta methods sub new_media { my $self = shift; chomp(my $name = shift); { my $try_rw; ($try_rw = $self->rewind) or return $try_rw; } return unless open my $handle , '>>' , $self->{device}; print $handle "$name\n"; close $handle; return 1; } sub write_index { my $self = shift; chomp(my @index = @_); unshift @index, $self->{mastername}; { my $try_ff; ($try_ff = $self->eod) or return $try_ff; } return unless open my $handle, '>>' , $self->{device}; print $handle map { "$_\n" } @index; close $handle; return 1; } sub get_index { my $self = shift; { my $try; ($try = $self->position_for_write) or return $try; } $self->status; return unless open ( my $handle, "<", $self->{device} ); my @index = <$handle>; close $handle; chomp(@index); $self->{mastername} = shift @index ; return @index; } sub position_for_write { my $self = shift; #MODIFIED. return $self->eod && $self->bsf(1) && $self->bsr(1); } sub goto { my $self = shift; my $pos = shift || return; # no point in goto if no position return $self->rewind() && $self->fsf($pos); } sub gotoend { my $self = shift; return $self->eod; } ################### workhorses sub execute { my $self = shift; $self->{error} = 0; $self->status; return if $self->{busy}; $self->{error} = system $self->{mt}, -f => $self->{device}, @_; #print Dumper @_; return $self->{error} == 0; } sub AUTOLOAD { my $self = shift; my $method = $AUTOLOAD; my $arg = shift; print"$arg\n"; $method =~ s/^.+:://; ( exists $self->{cmd}->{$method} ) and return $arg ? $self->execute($self->{cmd}->{$method}, $arg ) : $self->execute($self->{cmd}->{$method}); return; } 1;

      More comments.

      $self{qw(error cmd)} is of course just a typo that was meant to be @self{qw(error cmd)}.

      I suspect the and vs && issue is due to my not using parens there. Writing $self->endofdata() probably would fix it - if it does, I'd prefer that version as I find and a lot more readable.

      I specifically didn't want the assertions block to return $ERROR, because then you can say

      my $tape = Device::MagneticTape->new(..) or die Device::MagneticTape-> +error(); # instead of my $tape = Device::MagneticTape->new(..); die $tape unless ref $tape;

      The intent in the second version is not readily discernible, and $tape is not a name for something I'd expect to find an error message in. Actually, the point in going through the whole hoopla of writing the _assert() function was to be able to easily provide Device::MagneticTape->error() for the calling code.

      The $self{os} block malfunctions because of the dummy entry in %os_specific_cmd and due to scalar context forced by the || after the block. That piece of code will need some unravelling to function correctly..

      Not closing $rdh and $wrh in the status method shouldn't have any effect since they're lexicals - they go out of scope at the end of the function and are consequently autoclosed anyway. Is status behaving correctly? That's the one part of the code where I casted enough sorcery to be unsure whether it's bugfree with just a cursory look, but since as I said I don't have a tape drive I coudn't verify.

      Also, you are no longer calling execute from anywhere so you can roll it into AUTOLOAD.

      But as the module's user I would still like to have the long function names - I wouldn't want to have to learn the cryptic mt syntax. Besides, if this module is ever split to multiple backends so it can, f.ex, also control tapes on Windows with the native methods offered there, the mt syntax would be out of place. You want to offer your own consistent interface. But there's no reason why one shouldn't be able to do that using AUTOLOAD, and that way we also get rid of gotoend().

      So, another round:

      Please tell me how well these work.

      Makeshifts last the longest.

        Cool! - This _almost_ went through without a hitch Aristotle. But first with the changes I made.

        The os detection still needs some work, I think that
        my ($os) = grep $ostype =~ /$_/i, map quotemeta, keys %os_specific_cmd;
        is assigning the number of grep matches to $os, rather than $_ from a successful match.
        The assignment to @self{qw(error cmd)} required an extra set of curly-ones to enclose the hashes that are combined into $self->{cmd}.
        Fixed one typo ->{status} NOT ->{_status}.
        Despite what I believe to be valid syntax, the chaining with and is not honoured - even with parens after the method calls, so sadly these read &&.
        Used the @args variable within the scope of _meta because map complained about 'trying to modify readonly value' , which I presume to be @_.
        AUTOLOAD's method detection I have maybe over engineered, by adding $class, the regex also modified with the + immediatly after the character class, as opposed to after the parens.. (this would match only one char otherwise).

Log In?
Username:
Password:

What's my password?
Create A New User
Domain Nodelet?
Node Status?
node history
Node Type: perlmeditation [id://205936]
Approved by BrowserUk
help
Chatterbox?
and the web crawler heard nothing...

How do I use this?Last hourOther CB clients
Other Users?
Others taking refuge in the Monastery: (4)
As of 2024-04-24 11:54 GMT
Sections?
Information?
Find Nodes?
Leftovers?
    Voting Booth?

    No recent polls found