mirror of
https://github.com/ZoneMinder/zoneminder.git
synced 2026-03-29 11:12:04 -04:00
635 lines
16 KiB
Perl
635 lines
16 KiB
Perl
# ==========================================================================
|
|
#
|
|
# ZoneMinder Monitor Module, $Date$, $Revision$
|
|
# Copyright (C) 2001-2008 Philip Coombes
|
|
#
|
|
# 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.
|
|
#
|
|
# You should have received a copy of the GNU General Public License
|
|
# along with this program; if not, write to the Free Software
|
|
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
|
|
#
|
|
# ==========================================================================
|
|
#
|
|
# This module contains the common definitions and functions used by the rest
|
|
# of the ZoneMinder scripts
|
|
#
|
|
package ZoneMinder::Monitor;
|
|
|
|
use 5.006;
|
|
use strict;
|
|
use warnings;
|
|
use Time::HiRes qw(usleep);
|
|
|
|
require ZoneMinder::Base;
|
|
require ZoneMinder::Object;
|
|
require ZoneMinder::Storage;
|
|
require ZoneMinder::Server;
|
|
require ZoneMinder::Memory;
|
|
require ZoneMinder::Monitor_Status;
|
|
require ZoneMinder::Event_Summary;
|
|
require ZoneMinder::Zone;
|
|
require ZoneMinder::Manufacturer;
|
|
require ZoneMinder::Model;
|
|
use ZoneMinder::Logger qw(:all);
|
|
|
|
use parent qw(ZoneMinder::Object);
|
|
|
|
use vars qw/ $table $primary_key %fields $serial %defaults $debug/;
|
|
$debug = 0;
|
|
$table = 'Monitors';
|
|
$serial = $primary_key = 'Id';
|
|
%fields = map { $_ => $_ } qw(
|
|
Id
|
|
Name
|
|
Deleted
|
|
Notes
|
|
ServerId
|
|
StorageId
|
|
ManufacturerId
|
|
ModelId
|
|
Type
|
|
Capturing
|
|
Analysing
|
|
AnalysisSource
|
|
AnalysisImage
|
|
Recording
|
|
Decoding
|
|
JanusEnabled
|
|
JanusAudioEnabled
|
|
Janus_Profile_Override
|
|
Janus_Use_RTSP_Restream
|
|
Janus_RTSP_User
|
|
Janus_RTSP_Session_Timeout
|
|
LinkedMonitors
|
|
Triggers
|
|
EventStartCommand
|
|
EventEndCommand
|
|
ONVIF_URL
|
|
ONVIF_Username
|
|
ONVIF_Password
|
|
ONVIF_Options
|
|
ONVIF_Event_Listener
|
|
ONVIF_Alarm_Text
|
|
use_Amcrest_API
|
|
Device
|
|
Channel
|
|
Format
|
|
V4LMultiBuffer
|
|
V4LCapturesPerFrame
|
|
Protocol
|
|
Method
|
|
Host
|
|
Port
|
|
SubPath
|
|
Path
|
|
SecondPath
|
|
Options
|
|
User
|
|
Pass
|
|
Width
|
|
Height
|
|
Colours
|
|
Palette
|
|
Orientation
|
|
Deinterlacing
|
|
Decoder
|
|
DecoderHWAccelName
|
|
DecoderHWAccelDevice
|
|
SaveJPEGs
|
|
VideoWriter
|
|
OutputCodec
|
|
OutputContainer
|
|
EncoderParameters
|
|
RecordAudio
|
|
RecordingSource
|
|
RTSPDescribe
|
|
Brightness
|
|
Contrast
|
|
Hue
|
|
Colour
|
|
EventPrefix
|
|
LabelFormat
|
|
LabelX
|
|
LabelY
|
|
LabelSize
|
|
ImageBufferCount
|
|
MaxImageBufferCount
|
|
WarmupCount
|
|
PreEventCount
|
|
PostEventCount
|
|
StreamReplayBuffer
|
|
AlarmFrameCount
|
|
SectionLength
|
|
SectionLengthWarn
|
|
MinSectionLength
|
|
FrameSkip
|
|
MotionFrameSkip
|
|
AnalysisFPSLimit
|
|
AnalysisUpdateDelay
|
|
MaxFPS
|
|
AlarmMaxFPS
|
|
FPSReportInterval
|
|
RefBlendPerc
|
|
AlarmRefBlendPerc
|
|
Controllable
|
|
ControlId
|
|
ControlDevice
|
|
ControlAddress
|
|
AutoStopTimeout
|
|
TrackMotion
|
|
TrackDelay
|
|
ReturnLocation
|
|
ReturnDelay
|
|
ModectDuringPTZ
|
|
DefaultRate
|
|
DefaultScale
|
|
DefaultCodec
|
|
SignalCheckPoints
|
|
SignalCheckColour
|
|
WebColour
|
|
Exif
|
|
Sequence
|
|
ZoneCount
|
|
Refresh
|
|
DefaultCodec
|
|
Latitude
|
|
Longitude
|
|
RTSPServer
|
|
RTSPStreamName
|
|
Importance
|
|
MQTT_Enabled
|
|
MQTT_Subscriptions
|
|
);
|
|
|
|
# These have subroutines
|
|
$fields{Manufacturer} = undef;
|
|
$fields{manufacturer} = undef;
|
|
$fields{Model} = undef;
|
|
$fields{model} = undef;
|
|
|
|
%defaults = (
|
|
Name => q`'Monitor'`,
|
|
Deleted => 0,
|
|
ServerId => 0,
|
|
StorageId => 0,
|
|
ManufacturerId => undef,
|
|
ModelId => undef,
|
|
Type => q`'Ffmpeg'`,
|
|
Capturing => q`'Always'`,
|
|
Analysing => q`'Always'`,
|
|
AnalysisSource => q`'Primary'`,
|
|
AnalysisImage => q`'FullColour'`,
|
|
Recording => q`'Always'`,
|
|
Decoding => q`'Always'`,
|
|
JanusEnabled => 0,
|
|
JanusAudioEnabled => 0,
|
|
Janus_Profile_Override => q`''`,
|
|
Janus_Use_RTSP_Restream => 0,
|
|
Janus_RTSP_User => undef,
|
|
Janus_RTSP_Session_Timeout => 0,
|
|
LinkedMonitors => undef,
|
|
Triggers => '',
|
|
EventStartCommand => '',
|
|
EventEndCommand => '',
|
|
ONVIF_URL => q`''`,
|
|
ONVIF_Username => q`''`,
|
|
ONVIF_Password => q`''`,
|
|
ONVIF_Options => q`''`,
|
|
ONVIF_Event_Listener => 0,
|
|
ONVIF_Alarm_Text => q`'MotionAlarm'`,
|
|
use_Amcrest_API => 0,
|
|
Device => '',
|
|
Channel => 0,
|
|
Format => 0,
|
|
V4LMultiBuffer => undef,
|
|
V4LCapturesPerFrame => 1,
|
|
Protocol => undef,
|
|
Method => '',
|
|
Host => undef,
|
|
Port => '',
|
|
SubPath => '',
|
|
Path => undef,
|
|
SecondPath => undef,
|
|
Options => undef,
|
|
User => undef,
|
|
Pass => undef,
|
|
Width => 0,
|
|
Height => 0,
|
|
Colours => 4,
|
|
Palette => 0,
|
|
Orientation => q`'ROTATE_0'`,
|
|
Deinterlacing => 0,
|
|
Decoder => undef,
|
|
DecoderHWAccelName => undef,
|
|
DecoderHWAccelDevice => undef,
|
|
SaveJPEGs => 3,
|
|
VideoWriter => 0,
|
|
OutputCodec => 0,
|
|
OutputContainer => undef,
|
|
EncoderParameters => '',
|
|
RecordAudio=>0,
|
|
RecordingSource => q`'Primary'`,
|
|
RTSPDescribe=>0,
|
|
Brightness => -1,
|
|
Contrast => -1,
|
|
Hue => -1,
|
|
Colour => -1,
|
|
EventPrefix => q`'Event-'`,
|
|
LabelFormat => '',
|
|
LabelX => 0,
|
|
LabelY => 0,
|
|
LabelSize => 1,
|
|
ImageBufferCount => 20,
|
|
MaxImageBufferCount => 121,
|
|
WarmupCount => 0,
|
|
PreEventCount => 5,
|
|
PostEventCount => 5,
|
|
StreamReplayBuffer => 0,
|
|
AlarmFrameCount => 1,
|
|
SectionLength => 600,
|
|
SectionLengthWarn => 1,
|
|
MinSectionLength => 10,
|
|
FrameSkip => 0,
|
|
MotionFrameSkip => 0,
|
|
AnalysisFPSLimit => undef,
|
|
AnalysisUpdateDelay => 0,
|
|
MaxFPS => undef,
|
|
AlarmMaxFPS => undef,
|
|
FPSReportInterval => 100,
|
|
RefBlendPerc => 6,
|
|
AlarmRefBlendPerc => 6,
|
|
Controllable => 0,
|
|
ControlId => undef,
|
|
ControlDevice => undef,
|
|
ControlAddress => undef,
|
|
AutoStopTimeout => undef,
|
|
TrackMotion => 0,
|
|
TrackDelay => undef,
|
|
ReturnLocation => -1,
|
|
ReturnDelay => undef,
|
|
ModectDuringPTZ => 0,
|
|
DefaultRate => 100,
|
|
DefaultScale => 100,
|
|
DefaultCodec => q`'auto'`,
|
|
SignalCheckPoints => 0,
|
|
SignalCheckColour => q`'#0000BE'`,
|
|
WebColour => q`'#ff0000'`,
|
|
Exif => 0,
|
|
Sequence => undef,
|
|
ZoneCount => 0,
|
|
Refresh => undef,
|
|
DefaultCodec => q`'auto'`,
|
|
Latitude => undef,
|
|
Longitude => undef,
|
|
RTSPStreamName => '',
|
|
RTSPServer => 0,
|
|
Importance => q`'Normal'`,
|
|
MQTT_Enabled => 0,
|
|
MQTT_Subscriptions => q`''`,
|
|
);
|
|
|
|
use constant CAPTURING_NONE => 1;
|
|
use constant CAPTURING_ONDEMAND => 2;
|
|
use constant CAPTURING_ALWAYS => 3;
|
|
use constant ANALYSING_ALWAYS => 2;
|
|
use constant ANALYSING_NONE => 1;
|
|
|
|
sub save {
|
|
my $self = shift;
|
|
|
|
my $manufacturer = $self->Manufacturer();
|
|
my $model = $self->Model();
|
|
|
|
if ($manufacturer->Name() and !$manufacturer->Id()) {
|
|
if ($manufacturer->save()) {
|
|
$$self{ManufacturerId} = $manufacturer->Id();
|
|
if ($model->Name()) {
|
|
$model->ManufacturerId($$self{ManufacturerId});
|
|
}
|
|
}
|
|
}
|
|
if ($model->Name() and !$model->Id()) {
|
|
if ($model->save()) {
|
|
$$self{ModelId} = $model->Id()
|
|
}
|
|
}
|
|
|
|
my $error = $self->SUPER::save(@_);
|
|
return $error;
|
|
} # end sub save
|
|
|
|
sub Server {
|
|
return new ZoneMinder::Server( $_[0]{ServerId} );
|
|
} # end sub Server
|
|
|
|
sub Storage {
|
|
return new ZoneMinder::Storage( $_[0]{StorageId} );
|
|
} # end sub Storage
|
|
|
|
sub Zones {
|
|
if (! exists $_[0]{Zones}) {
|
|
$_[0]{Zones} = [ $_[0]{Id} ? ZoneMinder::Zone->find(MonitorId=>$_[0]{Id}) : () ];
|
|
}
|
|
return wantarray ? @{$_[0]{Zones}} : $_[0]{Zones};
|
|
}
|
|
|
|
sub control {
|
|
my $monitor = shift;
|
|
my $command = shift;
|
|
my $process = shift;
|
|
|
|
if ($command eq 'stop') {
|
|
if ($process) {
|
|
ZoneMinder::General::runCommand("zmdc.pl stop $process -m $$monitor{Id}");
|
|
} else {
|
|
if ($monitor->{Type} eq 'Local') {
|
|
ZoneMinder::General::runCommand('zmdc.pl stop zmc -d '.$monitor->{Device});
|
|
} else {
|
|
ZoneMinder::General::runCommand('zmdc.pl stop zmc -m '.$monitor->{Id});
|
|
}
|
|
}
|
|
} elsif ($command eq 'start') {
|
|
if ( $process ) {
|
|
ZoneMinder::General::runCommand("zmdc.pl start $process -m $$monitor{Id}");
|
|
} else {
|
|
if ($monitor->{Type} eq 'Local') {
|
|
ZoneMinder::General::runCommand('zmdc.pl start zmc -d '.$monitor->{Device});
|
|
} else {
|
|
ZoneMinder::General::runCommand('zmdc.pl start zmc -m '.$monitor->{Id});
|
|
}
|
|
} # end if
|
|
} elsif ( $command eq 'restart' ) {
|
|
if ( $process ) {
|
|
ZoneMinder::General::runCommand("zmdc.pl restart $process -m $$monitor{Id}");
|
|
} else {
|
|
if ($monitor->{Type} eq 'Local') {
|
|
ZoneMinder::General::runCommand('zmdc.pl restart zmc -d '.$monitor->{Device});
|
|
} else {
|
|
ZoneMinder::General::runCommand('zmdc.pl restart zmc -m '.$monitor->{Id});
|
|
}
|
|
}
|
|
}
|
|
} # end sub control
|
|
|
|
sub Status {
|
|
my $self = shift;
|
|
$$self{Status} = shift if @_;
|
|
if ( ! $$self{Status} ) {
|
|
$$self{Status} = ZoneMinder::Monitor_Status->find_one(MonitorId=>$$self{Id});
|
|
}
|
|
return $$self{Status};
|
|
}
|
|
|
|
sub Event_Summary {
|
|
my $self = shift;
|
|
$$self{Event_Summary} = shift if @_;
|
|
if ( ! $$self{Event_Summary} ) {
|
|
$$self{Event_Summary} = ZoneMinder::Event_Summary->find_one(MonitorId=>$$self{Id});
|
|
}
|
|
return $$self{Event_Summary};
|
|
}
|
|
|
|
sub connect {
|
|
my $self = shift;
|
|
if (!ZoneMinder::Memory::zmMemVerify($self)) {
|
|
$self->disconnect();
|
|
return undef;
|
|
}
|
|
return !undef;
|
|
}
|
|
|
|
sub disconnect {
|
|
my $self = shift;
|
|
ZoneMinder::Logger::Debug(4, "Disconnecting");
|
|
ZoneMinder::Memory::zmMemInvalidate($self); # Close our file handle to the zmc process we are about to end
|
|
}
|
|
|
|
sub suspendMotionDetection {
|
|
my $self = shift;
|
|
return 0 if ! ZoneMinder::Memory::zmMemVerify($self);
|
|
return if $$self{Capturing} eq 'None' or $$self{Analysing} eq 'None';
|
|
my $count = 50;
|
|
while ($count and
|
|
( ZoneMinder::Memory::zmMemRead($self, 'shared_data:analysing', 1) != ANALYSING_NONE)
|
|
) {
|
|
ZoneMinder::Logger::Debug(1, 'Suspending motion detection');
|
|
ZoneMinder::Memory::zmMonitorSuspend($self);
|
|
usleep(100000);
|
|
$count -= 1;
|
|
}
|
|
if (!$count) {
|
|
ZoneMinder::Logger::Error('Unable to suspend motion detection after 5 seconds.');
|
|
ZoneMinder::Memory::zmMemInvalidate($self); # Close our file handle to the zmc process we are about to end
|
|
} else {
|
|
ZoneMinder::Logger::Debug(1, 'shared_data:analysing='.ZoneMinder::Memory::zmMemRead($self, 'shared_data:analysing', 1));
|
|
}
|
|
}
|
|
|
|
sub resumeMotionDetection {
|
|
my $self = shift;
|
|
return 0 if ! ZoneMinder::Memory::zmMemVerify($self);
|
|
return if $$self{Capturing} eq 'None' or $$self{Analysing} eq 'None';
|
|
ZoneMinder::Logger::Debug(1, "capturing: $$self{Capturing} analysing: $$self{Analysing}");
|
|
my $count = 50;
|
|
while ($count and !ZoneMinder::Memory::zmMemRead($self, 'shared_data:analysing', 1)) {
|
|
ZoneMinder::Logger::Debug(1, 'Resuming motion detection');
|
|
ZoneMinder::Memory::zmMonitorResume($self);
|
|
usleep(100000);
|
|
$count -= 1;
|
|
}
|
|
if (!$count) {
|
|
ZoneMinder::Logger::Error('Unable to resume motion detection after 5 seconds.');
|
|
ZoneMinder::Memory::zmMemInvalidate($self); # Close our file handle to the zmc process we are about to end
|
|
}
|
|
return 1;
|
|
}
|
|
|
|
sub Control {
|
|
my $self = shift;
|
|
if (!exists $$self{Control}) {
|
|
if ($$self{ControlId}) {
|
|
require ZoneMinder::Control;
|
|
my $Control = ZoneMinder::Control->find_one(Id=>$$self{ControlId});
|
|
if ($Control) {
|
|
my $Protocol = $$Control{Protocol};
|
|
|
|
if (!$Protocol) {
|
|
Debug("No protocol set in control $$Control{Id}, trying Name $$Control{Name}");
|
|
$Protocol = $$Control{Name};
|
|
}
|
|
require Module::Load::Conditional;
|
|
if (!Module::Load::Conditional::can_load(modules => {'ZoneMinder::Control::'.$Protocol => undef})) {
|
|
Error("Monitor $$self{Id} $$self{Name} Can't load ZoneMinder::Control::$Protocol\n$Module::Load::Conditional::ERROR");
|
|
return undef;
|
|
}
|
|
$Control = $Control->clone(); # Because this object is not per monitor specific
|
|
bless $Control, 'ZoneMinder::Control::'.$Protocol;
|
|
$$Control{MonitorId} = $$self{Id};
|
|
$$Control{Monitor} = $self->clone();
|
|
$$self{Control} = $Control;
|
|
} else {
|
|
Error("Unable to load control for control $$self{ControlId} for monitor $$self{Id}");
|
|
}
|
|
} else {
|
|
Info("No ControlId set in monitor $$self{Id}")
|
|
}
|
|
}
|
|
return $$self{Control};
|
|
}
|
|
|
|
sub zmcControl {
|
|
my $self = shift;
|
|
my $mode = shift;
|
|
|
|
if (!$$self{Id}) {
|
|
Warning('Attempt to control a monitor with no Id');
|
|
return;
|
|
}
|
|
my $zmcArgs = '';
|
|
|
|
if ((!$ZoneMinder::Config{ZM_SERVER_ID}) or ( $$self{ServerId} and ($ZoneMinder::Config{ZM_SERVER_ID}==$$self{ServerId}) )) {
|
|
if ($$self{Type} eq 'Local') {
|
|
$zmcArgs .= '-d '.$self->{Device};
|
|
} else {
|
|
$zmcArgs .= '-m '.$self->{Id};
|
|
}
|
|
|
|
`/usr/bin/zmdc.pl $mode zmc $zmcArgs`;
|
|
} elsif ($self->ServerId()) {
|
|
my $Server = $self->Server();
|
|
|
|
my $url = $Server->UrlToApi().'/monitors/daemonControl/'.$self->{'Id'}.'/'.$mode.'/zmc.json';
|
|
if ($ZoneMinder::Config{ZM_OPT_USE_AUTH}) {
|
|
if ($ZoneMinder::Config{ZM_AUTH_RELAY} eq 'hashed') {
|
|
#$url .= '?auth='.generateAuthHash(ZM_AUTH_HASH_IPS);
|
|
} elsif ($ZoneMinder::Config{ZM_AUTH_RELAY} == 'plain') {
|
|
#$url .= '?user='.$_SESSION['username'];
|
|
#$url .= '?pass='.$_SESSION['password'];
|
|
} else {
|
|
Error('Multi-Server requires AUTH_RELAY be either HASH or PLAIN');
|
|
return;
|
|
}
|
|
}
|
|
Debug('sending command to '.$url);
|
|
my $ua = LWP::UserAgent->new();
|
|
$ua->ssl_opts(verify_hostname => 0, SSL_verify_mode => 0x00);
|
|
my $response = $ua->get($url);
|
|
if (!$response->is_success()) {
|
|
Error("Unable to restart remote monitor: " .$response->status_line);
|
|
}
|
|
} # end if local or remote
|
|
} # end sub zmcControl
|
|
|
|
sub ImportanceNumber {
|
|
my $self = shift;
|
|
if ($$self{Importance} eq 'Not') {
|
|
return 2;
|
|
} elsif ($$self{Importance} eq 'Less') {
|
|
return 1;
|
|
} elsif ($$self{Importance} eq 'Normal') {
|
|
return 0;
|
|
}
|
|
Warning("Weird value for Importance $$self{Importance} in monitor $$self{Id} $$self{Name}");
|
|
return 0;
|
|
}
|
|
|
|
sub Manufacturer {
|
|
my $self = shift;
|
|
$$self{Manufacturer} = shift if @_;
|
|
if (!$$self{Manufacturer}) {
|
|
$$self{Manufacturer} = new ZoneMinder::Manufacturer($$self{ManufacturerId});
|
|
}
|
|
return $$self{Manufacturer};
|
|
}
|
|
|
|
sub manufacturer {
|
|
my $self = shift;
|
|
if (@_) {
|
|
my $new = shift;
|
|
$$self{Manufacturer} = ZoneMinder::Manufacturer->find_one(Name=>$new);
|
|
if (!$$self{Manufacturer}) {
|
|
$$self{Manufacturer} = new ZoneMinder::Manufacturer();
|
|
$$self{Manufacturer}->Name($new);
|
|
}
|
|
$$self{ManufacturerId} = $$self{Manufacturer}->Id();
|
|
}
|
|
if (!$$self{Manufacturer}) {
|
|
$$self{Manufacturer} = new ZoneMinder::Manufacturer($$self{ManufacturerId});
|
|
}
|
|
return $$self{manufacturer} = $$self{Manufacturer}->Name();
|
|
}
|
|
|
|
sub Model {
|
|
my $self = shift;
|
|
if (@_) {
|
|
$$self{Model} = shift;
|
|
if (!$$self{Model}) {
|
|
$$self{Model} = new ZoneMinder::Model($$self{ModelId});
|
|
}
|
|
$$self{ModelId} = $$self{Model}->Id();
|
|
}
|
|
if (!$$self{Model}) {
|
|
$$self{Model} = new ZoneMinder::Model($$self{ModelId});
|
|
}
|
|
return $$self{Model};
|
|
}
|
|
|
|
sub model {
|
|
my $self = shift;
|
|
|
|
if (@_) {
|
|
my $new = shift;
|
|
if ((!$$self{Model}) or ($new ne $$self{Model}->Name())) {
|
|
$$self{Model} = ZoneMinder::Model->find_one(Name=>$new);
|
|
if (!$$self{Model}) {
|
|
$$self{Model} = new ZoneMinder::Model();
|
|
$$self{Model}->Name($new);
|
|
}
|
|
$$self{ModelId} = $$self{Model}->Id();
|
|
}
|
|
}
|
|
if (!$$self{Model}) {
|
|
$$self{Model} = new ZoneMinder::Model($$self{ModelId});
|
|
}
|
|
return $$self{Model}->Name();
|
|
}
|
|
|
|
sub DESTROY {
|
|
ZoneMinder::Memory::zmMemInvalidate($_[0]) if ZoneMinder::Memory::zmMemKey($_[0]);
|
|
}
|
|
|
|
1;
|
|
__END__
|
|
|
|
=head1 NAME
|
|
|
|
ZoneMinder::Monitor - Perl Class for Monitors
|
|
|
|
=head1 SYNOPSIS
|
|
|
|
use ZoneMinder::Monitor;
|
|
|
|
=head1 AUTHOR
|
|
|
|
Isaac Connor, E<lt>isaac@zoneminder.comE<gt>
|
|
|
|
=head1 COPYRIGHT AND LICENSE
|
|
|
|
Copyright (C) 2001-2017 ZoneMinder LLC
|
|
|
|
This library is free software; you can redistribute it and/or modify
|
|
it under the same terms as Perl itself, either Perl version 5.8.3 or,
|
|
at your option, any later version of Perl 5 you may have available.
|
|
|
|
|
|
=cut
|