fhem-mirror/FHEM/98_Installer.pm
jpawlowski 1828c5e6bb 98_Installer: update to v0.5.7
git-svn-id: https://svn.fhem.de/fhem/trunk/fhem@20949 2b470e98-0d58-463d-a4d8-8e2adae1ed80
2020-01-12 09:53:11 +00:00

5599 lines
184 KiB
Perl

# $Id$
package main;
use strict;
use warnings;
use POSIX;
use FHEM::Meta;
sub Installer_Initialize($) {
my ($modHash) = @_;
$modHash->{SetFn} = "FHEM::Installer::Set";
$modHash->{GetFn} = "FHEM::Installer::Get";
$modHash->{DefFn} = "FHEM::Installer::Define";
$modHash->{NotifyFn} = "FHEM::Installer::Notify";
$modHash->{UndefFn} = "FHEM::Installer::Undef";
$modHash->{AttrFn} = "FHEM::Installer::Attr";
$modHash->{AttrList} =
"disable:1,0 "
. "disabledForIntervals "
. "installerMode:update,developer "
. "updateListReading:1,0 "
. "updatePerlDualLifeModules:0,1 "
. "updatePerlUsedModulesOnly:1,0 "
. "installPerlReinstall:0,1 "
. "installPerlNoTest:0,1 "
. "installPerlEnforced:0,1 "
. $readingFnAttributes;
return FHEM::Meta::InitMod( __FILE__, $modHash );
}
# define package
package FHEM::Installer;
use strict;
use warnings;
use POSIX;
use FHEM::Meta;
use GPUtils qw(GP_Import);
use Data::Dumper;
use Config;
use ExtUtils::Installed;
# Run before module compilation
BEGIN {
# Import from main::
GP_Import(
qw(
attr
AttrVal
cmds
CommandAttr
Debug
defs
deviceEvents
devspec2array
DoTrigger
FW_webArgs
gettimeofday
init_done
InternalTimer
IsDisabled
LoadModule
Log
Log3
maxNum
modules
packages
readingsBeginUpdate
readingsBulkUpdate
readingsBulkUpdateIfChanged
readingsEndUpdate
readingsSingleUpdate
ReadingsTimestamp
ReadingsVal
RemoveInternalTimer
TimeNow
Value
)
);
}
# try to use JSON::MaybeXS wrapper
# for chance of better performance + open code
eval {
require JSON::MaybeXS;
import JSON::MaybeXS qw( decode_json encode_json );
1;
};
if ($@) {
$@ = undef;
# try to use JSON wrapper
# for chance of better performance
eval {
# JSON preference order
local $ENV{PERL_JSON_BACKEND} =
'Cpanel::JSON::XS,JSON::XS,JSON::PP,JSON::backportPP'
unless ( defined( $ENV{PERL_JSON_BACKEND} ) );
require JSON;
import JSON qw( decode_json encode_json );
1;
};
if ($@) {
$@ = undef;
# In rare cases, Cpanel::JSON::XS may
# be installed but JSON|JSON::MaybeXS not ...
eval {
require Cpanel::JSON::XS;
import Cpanel::JSON::XS qw(decode_json encode_json);
1;
};
if ($@) {
$@ = undef;
# In rare cases, JSON::XS may
# be installed but JSON not ...
eval {
require JSON::XS;
import JSON::XS qw(decode_json encode_json);
1;
};
if ($@) {
$@ = undef;
# Fallback to built-in JSON which SHOULD
# be available since 5.014 ...
eval {
require JSON::PP;
import JSON::PP qw(decode_json encode_json);
1;
};
if ($@) {
$@ = undef;
# Fallback to JSON::backportPP in really rare cases
require JSON::backportPP;
import JSON::backportPP qw(decode_json encode_json);
1;
}
}
}
}
}
# Load dependent FHEM modules as packages,
# no matter if user also defined FHEM devices or not.
# We want to use their functions here :-)
#TODO let this make Meta.pm for me
#LoadModule('apt');
#LoadModule('pypip');
LoadModule('npmjs');
our %pkgStatus = ();
sub Define($$) {
my ( $hash, $def ) = @_;
my @a = split( "[ \t][ \t]*", $def );
# Initialize the module and the device
return $@ unless ( FHEM::Meta::SetInternals($hash) );
use version 0.77; our $VERSION = FHEM::Meta::Get( $hash, 'version' );
my $name = $a[0];
my $host = $a[2] ? $a[2] : 'localhost';
Undef( $hash, undef ) if ( $hash->{OLDDEF} ); # modify
$hash->{NOTIFYDEV} = "global,$name";
return "Existing instance: "
. $modules{ $hash->{TYPE} }{defptr}{localhost}{NAME}
if ( defined( $modules{ $hash->{TYPE} }{defptr}{localhost} ) );
$modules{ $hash->{TYPE} }{defptr}{localhost} = $hash;
if ( $init_done && !defined( $hash->{OLDDEF} ) ) {
# presets for FHEMWEB
$attr{$name}{alias} = 'FHEM Installer Status';
$attr{$name}{devStateIcon} =
'.*updates.available:security@red:outdated up.to.date:security@green:outdated .*outdated.*in.progress:system_fhem_reboot@orange .*in.progress:system_fhem_update@orange warning.*:message_attention@orange error.*:message_attention@red';
$attr{$name}{group} = 'Update';
$attr{$name}{icon} = 'system_fhem';
$attr{$name}{room} = 'System';
}
readingsSingleUpdate( $hash, "state", "initialized", 1 )
if ( ReadingsVal( $name, 'state', 'none' ) ne 'none' );
return undef;
}
sub Undef($$) {
my ( $hash, $arg ) = @_;
my $name = $hash->{NAME};
if ( exists( $hash->{".fhem"}{subprocess} ) ) {
my $subprocess = $hash->{".fhem"}{subprocess};
$subprocess->terminate();
$subprocess->wait();
}
RemoveInternalTimer($hash);
delete( $modules{ $hash->{TYPE} }{defptr}{localhost} );
return undef;
}
sub Attr(@) {
my ( $cmd, $name, $attrName, $attrVal ) = @_;
my $hash = $defs{$name};
if ( $attrName eq "disable" ) {
if ( $cmd eq "set" and $attrVal eq "1" ) {
RemoveInternalTimer($hash);
readingsSingleUpdate( $hash, "state", "disabled", 1 );
Log3 $name, 3, "Installer ($name) - disabled";
}
elsif ( $cmd eq "del" ) {
Log3 $name, 3, "Installer ($name) - enabled";
}
}
elsif ( $attrName eq "disabledForIntervals" ) {
if ( $cmd eq "set" ) {
return
"check disabledForIntervals Syntax HH:MM-HH:MM or 'HH:MM-HH:MM HH:MM-HH:MM ...'"
unless ( $attrVal =~ /^((\d{2}:\d{2})-(\d{2}:\d{2})\s?)+$/ );
Log3 $name, 3, "Installer ($name) - disabledForIntervals";
readingsSingleUpdate( $hash, "state", "disabled", 1 );
}
elsif ( $cmd eq "del" ) {
Log3 $name, 3, "Installer ($name) - enabled";
readingsSingleUpdate( $hash, "state", "active", 1 );
}
}
return undef;
}
sub Notify($$) {
my ( $hash, $dev ) = @_;
my $name = $hash->{NAME};
return if ( IsDisabled($name) );
my $devname = $dev->{NAME};
my $devtype = $dev->{TYPE};
my $events = deviceEvents( $dev, 1 );
return if ( !$events );
Log3 $name, 5, "Installer ($name) - Notify: " . Dumper $events;
if (
(
(
grep ( /^DEFINED.$name$/, @{$events} )
or grep ( /^DELETEATTR.$name.disable$/, @{$events} )
or grep ( /^ATTR.$name.disable.0$/, @{$events} )
)
and $devname eq 'global'
and $init_done
)
or (
(
grep ( /^INITIALIZED$/, @{$events} )
or grep ( /^REREADCFG$/, @{$events} )
or grep ( /^MODIFIED.$name$/, @{$events} )
)
and $devname eq 'global'
)
)
{
# Load metadata for all modules that are in use
FHEM::Meta::Load();
# restore from packageList
my $decode_json =
eval { decode_json( ReadingsVal( $name, '.packageListPerl', '' ) ) };
unless ($@) {
$hash->{".fhem"}{installer}{cpanversions} = $decode_json->{versions}
if ( defined( $decode_json->{versions} ) );
$hash->{".fhem"}{installer}{listedPerlPackages} =
$decode_json->{listedPerl}
if ( defined( $decode_json->{listedPerl} ) );
$hash->{".fhem"}{installer}{outdatedPerlPackages} =
$decode_json->{outdatedPerl}
if ( defined( $decode_json->{outdatedPerl} ) );
}
$decode_json = undef;
# restore from installedList
$decode_json =
eval { decode_json( ReadingsVal( $name, '.installedListPerl', '' ) ) };
unless ($@) {
$hash->{".fhem"}{installer}{installedPerlPackages} = $decode_json;
}
$decode_json = undef;
# restore from uninstalledList
$decode_json = eval {
decode_json( ReadingsVal( $name, '.uninstalledListPerl', '' ) );
};
unless ($@) {
$hash->{".fhem"}{installer}{uninstalledPerlPackages} = $decode_json;
}
$decode_json = undef;
# restore from updatedList
$decode_json =
eval { decode_json( ReadingsVal( $name, '.updatedListPerl', '' ) ) };
unless ($@) {
$hash->{".fhem"}{installer}{updatedPerlPackages} = $decode_json;
}
$decode_json = undef;
# Trigger update
if ( ReadingsVal( $name, 'cpanVersion', 'none' ) ne 'none' ) {
ProcessUpdateTimer($hash);
}
else {
$hash->{".fhem"}{installer}{cmd} = 'getCpanVersion';
AsynchronousExecuteFhemCommand($hash);
}
}
if (
$devname eq $name
and ( grep ( /^installedPerl:.successful$/, @{$events} )
or grep ( /^uninstalledPerl:.successful$/, @{$events} )
or grep ( /^updatedPerl:.successful$/, @{$events} ) )
)
{
$hash->{".fhem"}{installer}{cmd} = 'outdatedPerl';
AsynchronousExecuteFhemCommand($hash);
}
return;
}
sub Set($$@) {
my ( $hash, $name, @aa ) = @_;
my ( $cmd, @args ) = @aa;
my $ret;
my $updatePerlDualLifeModules =
AttrVal( $name, 'updatePerlDualLifeModules', 0 );
my $updatePerlUsedModulesOnly =
AttrVal( $name, 'updatePerlUsedModulesOnly', 1 );
# outdatedPerl
if ( lc($cmd) eq 'outdatedperl' ) {
$hash->{".fhem"}{installer}{cmd} = $cmd;
}
# statusRequest
elsif ( lc($cmd) eq 'statusrequest' ) {
$hash->{".fhem"}{installer}{cmd} = 'getCpanVersion';
}
# update
elsif ( lc($cmd) eq 'updateperl' ) {
return "Please run outdatedPerl check first"
unless (
defined( $hash->{".fhem"}{installer}{outdatedPerlPackages} ) );
my $update;
# generate explicit list for packages to update
if ( @args == 0
|| lc( $args[0] ) eq 'all' )
{
undef @args;
foreach (
keys %{ $hash->{".fhem"}{installer}{outdatedPerlPackages} } )
{
next if ( $_ eq 'undefined' );
if ( $updatePerlDualLifeModules
|| !FHEM::Meta::ModuleIsPerlCore($_) )
{
push @args,
$_ . '@'
. $hash->{".fhem"}{installer}{outdatedPerlPackages}{$_}
{latest}
if ( !$updatePerlUsedModulesOnly
|| defined( $FHEM::Meta::dependents{pkgs}{$_} ) );
}
}
}
foreach my $pkgfull (@args) {
next
unless (
$pkgfull =~
/^(?:@([\w-]+)\/)?([\w\-\:]+)(?:@([\d\.=<>]+|latest))?$/
&& defined(
$hash->{".fhem"}{installer}{outdatedPerlPackages}{$2}
)
);
$update .= " " if ($update);
$update .= $2 . ( $3 && $3 ne 'latest' ? '@' . $3 : '' );
}
return "Everything is up-to-date already"
unless ($update);
$hash->{".fhem"}{installer}{cmd} = "installPerl " . $update;
$ret = "Update started in background";
}
# installPerl
elsif ( lc($cmd) eq 'installperl' ) {
return "usage: $cmd <package>" if ( @args < 1 );
$hash->{".fhem"}{installer}{cmd} = 'installPerl ' . join( " ", @args );
$ret = "Installation started in background";
}
# uninstallPerl
elsif ( lc($cmd) eq 'uninstallperl' ) {
return "usage: $cmd <package>" if ( @args < 1 );
return "cpanminus cannot be uninstalled from here"
if (
grep (
m/^(?:@([\w-]+)\/)?(?:App::)?(cpanm(?:inus)?)(?:@([\d\.=<>]+|latest))?$/i,
@args )
);
return "cpan-outdated cannot be uninstalled from here"
if (
grep (
m/^(?:@([\w-]+)\/)?(?:App::)?(cpan\-?outdated)(?:@([\d\.=<>]+|latest))?$/i,
@args )
);
$hash->{".fhem"}{installer}{cmd} =
'uninstallPerl ' . join( " ", @args );
$ret = "Deinstallation started in background";
}
# return Usage:
else {
my $list = '';
if ( !defined( $hash->{".fhem"}{installer}{cpanversions} ) ) {
$list = "installPerl:App::cpanminus statusRequest:noArg";
}
elsif (
!defined( $hash->{".fhem"}{installer}{cpanversions}{cpanoutdated} )
)
{
$list = "installPerl:App::cpanoutdated statusRequest:noArg";
}
else {
$list = "outdatedPerl:noArg";
$list .= " installPerl";
$list .= " uninstallPerl";
if ( defined( $hash->{".fhem"}{installer}{outdatedPerlPackages} )
and scalar
keys %{ $hash->{".fhem"}{installer}{outdatedPerlPackages} } >
0 )
{
my $update;
foreach (
sort
keys %{ $hash->{".fhem"}{installer}{outdatedPerlPackages} }
)
{
if (
(
$updatePerlDualLifeModules
|| !FHEM::Meta::ModuleIsPerlCore($_)
)
&& ( !$updatePerlUsedModulesOnly
|| defined( $FHEM::Meta::dependents{pkgs}{$_} ) )
)
{
$update .= "," if ($update);
$update .= $_;
}
}
$list .= " updatePerl:all,$update" if ($update);
}
}
return "Unknown argument $cmd, choose one of $list";
}
AsynchronousExecuteFhemCommand($hash);
return $ret if ($ret);
return undef;
}
#TODO
# - filter out FHEM command modules from FHEMWEB view (+attribute) -> difficult as not pre-loaded
sub Get($$@) {
my ( $hash, $name, @aa ) = @_;
my ( $cmd, @args ) = @aa;
my $updatePerlDualLifeModules =
AttrVal( $name, 'updatePerlDualLifeModules', 0 );
my $updatePerlUsedModulesOnly =
AttrVal( $name, 'updatePerlUsedModulesOnly', 1 );
if ( lc($cmd) eq 'showoutdatedperllist' ) {
return "usage: $cmd" if ( @args != 0 );
my $ret = CreateOutdatedPerlList( $hash, $cmd );
return $ret;
}
elsif ( lc($cmd) eq 'showinstalledperllist' ) {
return "usage: $cmd" if ( @args != 0 );
my $ret = CreateInstalledPerlList( $hash, $cmd );
return $ret;
}
elsif ( lc($cmd) eq 'checkprereqs' ) {
my $ret = CreatePrereqsList( $hash, $cmd, @args );
return $ret;
}
elsif ( lc($cmd) eq 'search' ) {
my $ret = CreateSearchList( $hash, $cmd, @args );
return $ret;
}
elsif ( lc($cmd) eq 'showmoduleinfo' ) {
return "usage: $cmd MODULE" if ( @args != 1 );
my $ret = CreateMetadataList( $hash, $cmd, $args[0] );
return $ret;
}
elsif ( lc($cmd) eq 'showpackageinfo' ) {
return "usage: $cmd PACKAGE" if ( @args != 1 );
my $ret = CreateMetadataList( $hash, $cmd, $args[0] );
return $ret;
}
elsif ( lc($cmd) eq 'zzgetmodulemeta.json' ) {
return "usage: $cmd MODULE" if ( @args != 1 );
my $ret = CreateRawMetaJson( $hash, $cmd, $args[0] );
return $ret;
}
elsif ( lc($cmd) eq 'zzgetpackagemeta.json' ) {
return "usage: $cmd PACKAGE" if ( @args != 1 );
my $ret = CreateRawMetaJson( $hash, $cmd, $args[0] );
return $ret;
}
elsif ( lc($cmd) eq 'showerrorlist' ) {
return "usage: $cmd" if ( @args != 0 );
my $ret = CreateErrorList($hash);
return $ret;
}
else {
my $installerMode = AttrVal( $name, 'installerMode', 'update' );
my @fhemModules;
foreach ( sort { "\L$a" cmp "\L$b" } keys %modules ) {
next if ( $_ eq 'Global' );
push @fhemModules, $_
if ( $installerMode ne 'update'
|| defined( $modules{$_}{LOADED} ) );
}
my $list =
'search' . ' showModuleInfo:FHEM,' . join( ',', @fhemModules );
if ( $installerMode eq 'developer' ) {
my @fhemPackages;
foreach ( sort { "\L$a" cmp "\L$b" } keys %packages ) {
push @fhemPackages, $_;
}
$list .=
' showPackageInfo:'
. join( ',', @fhemPackages )
. ' zzGetModuleMETA.json:FHEM,'
. join( ',', @fhemModules )
. ' zzGetPackageMETA.json:'
. join( ',', @fhemPackages );
}
$list .= " checkPrereqs";
if ( $installerMode eq 'install' ) {
my $dh;
my $dir = $attr{global}{modpath};
if ( opendir( $dh, $dir ) ) {
my $counter = 0;
foreach my $fn (
grep { $_ ne "." && $_ ne ".." && !-d $_ && $_ =~ /\.cfg$/ }
readdir($dh)
)
{
$list .= ':' unless ($counter);
$list .= ',' if ($counter);
$list .= $fn;
$counter++;
}
closedir($dh);
}
}
elsif ( $installerMode eq 'update' ) {
$list .= ':noArg';
}
my $counter = 0;
if ( defined( $hash->{".fhem"}{installer}{outdatedPerlPackages} ) ) {
foreach (
keys %{ $hash->{".fhem"}{installer}{outdatedPerlPackages} } )
{
$counter++
if (
(
$updatePerlDualLifeModules
|| !FHEM::Meta::ModuleIsPerlCore($_)
)
&& ( !$updatePerlUsedModulesOnly
|| defined( $FHEM::Meta::dependents{pkgs}{$_} ) )
);
}
}
$list .= " showOutdatedPerlList:noArg"
if ($counter);
$list .= " showInstalledPerlList:noArg"
if ( defined( $hash->{".fhem"}{installer}{listedPerlPackages} )
and scalar
keys %{ $hash->{".fhem"}{installer}{listedPerlPackages} } > 0 );
$list .= " showErrorList:noArg"
if ( defined( $hash->{".fhem"}{installer}{errors} )
and scalar @{ $hash->{".fhem"}{installer}{errors} } > 0 );
return "Unknown argument $cmd, choose one of $list";
}
}
sub Event ($$) {
my $hash = shift;
my $event = shift;
my $name = $hash->{NAME};
return
unless ( defined( $hash->{".fhem"}{installer}{cmd} )
&& $hash->{".fhem"}{installer}{cmd} =~
m/^(install|uninstall|update)(?: (.+))/i );
my $cmd = $1;
my $pkgs = $2;
my $list;
foreach my $package ( split / /, $pkgs ) {
next
unless ( $package =~
/^(?:@([\w-]+)\/)?([\w\-\:]+)(?:@([\d\.=<>]+|latest))?$/ );
$list .= " " if ($list);
$list .= $2;
}
DoModuleTrigger( $hash, uc($event) . uc($cmd) . " $name $list" );
}
sub DoModuleTrigger($$@) {
my ( $hash, $eventString, $noreplace, $TYPE ) = @_;
$hash = $defs{$hash} unless ( ref($hash) );
$noreplace = 1 unless ( defined($noreplace) );
$TYPE = $hash->{TYPE} unless ( defined($TYPE) );
return ''
unless ( defined($TYPE)
&& defined( $modules{$TYPE} )
&& defined($eventString)
&& $eventString =~
m/^([A-Za-z\d._]+)(?:\s+([A-Za-z\d._]+)(?:\s+(.+))?)?$/ );
my $event = $1;
my $dev = $2;
return "DoModuleTrigger() can only handle module related events"
if ( ( $hash->{NAME} && $hash->{NAME} eq "global" )
|| $dev eq "global" );
# This is a global event on module level
return DoTrigger( "global", "$TYPE:$eventString", $noreplace )
unless ( $event =~
/^INITIALIZED|INITIALIZING|MODIFIED|DELETED|BEGIN(?:UPDATE|INSTALL|UNINSTALL)|END(?:UPDATE|INSTALL|UNINSTALL)$/
);
# This is a global event on module level and in device context
return "$event: missing device name"
if ( !defined($dev) || $dev eq '' );
return DoTrigger( "global", "$TYPE:$eventString", $noreplace );
}
###################################
sub ProcessUpdateTimer($) {
my $hash = shift;
my $name = $hash->{NAME};
RemoveInternalTimer($hash);
InternalTimer(
gettimeofday() + 14400,
"FHEM::Installer::ProcessUpdateTimer",
$hash, 0
);
Log3 $name, 4, "Installer ($name) - stateRequestTimer: Call Request Timer";
unless ( IsDisabled($name) ) {
if ( exists( $hash->{".fhem"}{subprocess} ) ) {
Log3 $name, 2,
"Installer ($name) - update in progress, process aborted.";
return 0;
}
readingsSingleUpdate( $hash, "state", "ready", 1 )
if ( ReadingsVal( $name, 'state', 'none' ) eq 'none'
or ReadingsVal( $name, 'state', 'none' ) eq 'initialized' );
if (
__ToDay() ne (
split(
' ',
ReadingsTimestamp( $name, 'outdatedPerl', '1970-01-01' )
)
)[0]
or ReadingsVal( $name, 'state', '' ) eq 'disabled'
)
{
$hash->{".fhem"}{installer}{cmd} = 'outdatedPerl';
AsynchronousExecuteFhemCommand($hash);
}
}
}
sub CleanSubprocess($) {
my $hash = shift;
my $name = $hash->{NAME};
delete( $hash->{".fhem"}{subprocess} );
Log3 $name, 4, "Installer ($name) - clean Subprocess";
}
use constant POLLINTERVAL => 1;
sub AsynchronousExecuteFhemCommand($) {
require "SubProcess.pm";
my ($hash) = shift;
my $name = $hash->{NAME};
my $subprocess = SubProcess->new( { onRun => \&OnRun } );
$subprocess->{installer} = $hash->{".fhem"}{installer};
$subprocess->{installer}{debug} =
( AttrVal( $name, 'verbose', 0 ) > 3 ? 1 : 0 );
$subprocess->{installer}{installPerlReinstall} =
AttrVal( $name, 'installPerlReinstall', 0 ) ? '--reinstall ' : '';
$subprocess->{installer}{installPerlNoTest} =
AttrVal( $name, 'installPerlNoTest', 0 ) ? '--notest ' : '';
$subprocess->{installer}{installPerlEnforced} =
AttrVal( $name, 'installPerlEnforced', 0 ) ? '--force ' : '';
my $pid = $subprocess->run();
readingsSingleUpdate( $hash, 'state',
'command \'' . $hash->{".fhem"}{installer}{cmd} . '\' in progress', 1 );
if ( !defined($pid) ) {
Log3 $name, 1,
"Installer ($name) - Cannot execute command asynchronously";
CleanSubprocess($hash);
readingsSingleUpdate( $hash, 'state',
'Cannot execute command asynchronously', 1 );
return undef;
}
Event( $hash, "BEGIN" );
Log3 $name, 4,
"Installer ($name) - execute command asynchronously (PID= $pid)";
$hash->{".fhem"}{subprocess} = $subprocess;
InternalTimer( gettimeofday() + POLLINTERVAL,
"FHEM::Installer::PollChild", $hash, 0 );
Log3 $hash, 4, "Installer ($name) - control passed back to main loop.";
}
sub PollChild($) {
my $hash = shift;
my $name = $hash->{NAME};
if ( !exists( $hash->{".fhem"}{subprocess} )
|| !ref( $hash->{".fhem"}{subprocess} )
|| ref( $hash->{".fhem"}{subprocess} ) ne 'SubProcess' )
{
my $emsg =
defined( $hash->{".fhem"}{subprocess} )
&& $hash->{".fhem"}{subprocess}{lasterror} ne ''
? '{"error":"' . $hash->{".fhem"}{subprocess}{lasterror} . '"}'
: '{"error":"Child process suddenly ended"}';
Log3 $name, 5, "Installer ($name) - $emsg";
CleanSubprocess($hash);
PreProcessing( $hash, $emsg );
return;
}
my $subprocess = $hash->{".fhem"}{subprocess};
my $json = $subprocess->readFromChild();
if ( !defined($json) ) {
Log3 $name, 5,
"Installer ($name) - still waiting ("
. $subprocess->{lasterror} . ").";
InternalTimer( gettimeofday() + POLLINTERVAL,
"FHEM::Installer::PollChild", $hash, 0 );
return;
}
else {
Log3 $name, 4,
"Installer ($name) - got result from asynchronous parsing.";
$subprocess->wait();
Log3 $name, 4, "Installer ($name) - asynchronous finished.";
CleanSubprocess($hash);
PreProcessing( $hash, $json );
}
}
######################################
# Begin Childprocess
######################################
sub OnRun() {
my $subprocess = shift;
my $response = ExecuteFhemCommand( $subprocess->{installer} );
my $json = eval { encode_json($response) };
if ($@) {
Log3 'Installer OnRun', 3, "Installer - JSON error: $@";
$json = "{\"jsonerror\":\"$@\"}";
}
$subprocess->writeToParent($json);
}
sub ExecuteFhemCommand($) {
my $cmd = shift;
my $installer = {};
$installer->{debug} = $cmd->{debug};
my $locale = 'LC_ALL=C';
my $sudo = $locale . ' sudo -n ';
my $sudoH = $locale . ' sudo -H -n ';
$installer->{cpanversions} =
'echo n | TEST=$(which cpanm) || echo "sh: command not found: cpanm"; which cpanm >/dev/null 2>&1 && sh -c "'
. $sudoH
. ' $(which cpanm) --version 2>&1" 2>&1';
$installer->{installperl} =
'echo n | sh -c "'
. $sudoH
. ' $(which cpanm) --quiet '
. $cmd->{installPerlReinstall}
. $cmd->{installPerlNoTest}
. $cmd->{installPerlEnforced}
. '%PACKAGES%" 2>&1';
$installer->{uninstallperl} =
'echo n | sh -c "'
. $sudoH
. ' $(which cpanm) -U --quiet --force %PACKAGES%" 2>&1';
$installer->{outdatedperl} =
'echo n | '
. 'sh -c "'
. $sudoH
. ' $(which cpanm) --version 2>&1" 2>&1 && ' . 'L1=$('
. ' cpan-outdated --verbose 2>&1) && '
. '[ "$L1" != "" ] && [ "$L1" != "\n" ] && echo "@Outdated:\n$L1"; ';
my $response;
if ( $cmd->{cmd} =~ /^installperl (.+)/i ) {
if ( not defined( $cmd->{cpanversions} )
or not defined( $cmd->{cpanversions}{cpanminus} ) )
{
if ( $1 =~ /App::cpanminus/i ) {
$installer->{installperl} =
'sh -c "( curl -fsSL https://git.io/cpanm 2>/dev/null || wget -qO- https://git.io/cpanm 2>/dev/null ) | '
. $sudoH
. '$(which perl) - App::cpanminus >/dev/null 2>&1" 2>&1 '
. '&& TEST=$(which cpanm) || echo "sh: command not found: cpanm"; which cpanm >/dev/null 2>&1 && sh -c "'
. $sudoH
. ' $(which cpanm) --quiet App::cpanoutdated" 2>&1';
}
}
else {
my @packages = '';
foreach my $package ( split / /, $1 ) {
next
unless ( $package =~
/^(?:@([\w-]+)\/)?([\w\-\:]+)(?:@([\d\.=<>]+|latest))?$/ );
push @packages, $2 . ( $3 && $3 ne 'latest' ? '@' . $3 : '' );
}
my $pkglist = join( ' ', @packages );
return unless ( $pkglist ne '' );
$installer->{installperl} =~ s/%PACKAGES%/$pkglist/gi;
}
print qq($installer->{installperl}\n)
if ( $installer->{debug} == 1 );
$response = CpanInstall($installer);
}
elsif ( $cmd->{cmd} =~ /^uninstallperl (.+)/i ) {
my @packages = '';
foreach my $package ( split / /, $1 ) {
next
unless ( $package =~
/^(?:@([\w-]+)\/)?([\w\-\:]+)(?:@([\d\.=<>]+|latest))?$/ );
push @packages, $2;
}
my $pkglist = join( ' ', @packages );
return unless ( $pkglist ne '' );
$installer->{uninstallperl} =~ s/%PACKAGES%/$pkglist/gi;
print qq($installer->{uninstallperl}\n)
if ( $installer->{debug} == 1 );
$response = CpanUninstall($installer);
}
elsif ( lc( $cmd->{cmd} ) eq 'outdatedperl' ) {
print qq($installer->{outdatedperl}\n)
if ( $installer->{debug} == 1 );
$response = CpanOutdated($installer);
}
elsif ( lc( $cmd->{cmd} ) eq 'getcpanversion' ) {
print qq($installer->{cpanversions}\n)
if ( $installer->{debug} == 1 );
$response = GetCpanVersion($installer);
}
return $response;
}
sub GetCpanVersion($) {
my $cmd = shift;
my $h = {};
local $ENV{PATH} = __GetExtendedEnvPath();
my $p = `$cmd->{cpanversions}`;
my $found = 0;
my $isConfig = 0;
my $isEnv = 0;
my $isInc = 0;
if ( $p && $p ne '' ) {
foreach my $line ( split /\n/, $p ) {
chomp($line);
print qq($line\n) if ( $cmd->{debug} == 1 );
if ( $line =~
/^cpanm (?:\([A-za-z:]+\) )?version (\d+\.\d+) (\([\w\/\.\-]+\)).*/i
)
{
$h->{versions}{cpanminus} = $1;
$found = 1;
}
elsif ( $line =~ /^perl version (\d+\.\d+) (\([\w\/\.\-]+\)).*/i ) {
$h->{versions}{perl} = $1;
}
elsif ( $line =~ /^\s+\%Config:.*$/i ) {
$isConfig = 1;
}
elsif ( $line =~ /^\s+\%ENV:.*$/i ) {
$isConfig = 0;
$isEnv = 1;
}
elsif ( $line =~ /^\s+\@INC:.*$/i ) {
$isConfig = 0;
$isEnv = 0;
$isInc = 1;
}
elsif ($isConfig) {
$line =~ s/^\s+//g;
my @splitted = split( /=/, $line, 2 );
$h->{versions}{Config}{ $splitted[0] } = $splitted[1];
}
elsif ($isEnv) {
$line =~ s/^\s+//g;
my @splitted = split( /=/, $line, 2 );
$h->{versions}{ENV}{ $splitted[0] } = $splitted[1];
}
elsif ($isInc) {
$line =~ s/^\s+//g;
push @{ $h->{versions}{INC} }, $line
unless ( $line =~ /^.+=.+$/i );
}
# error
elsif ( !$found ) {
my $error = {};
if ( $line =~
m/(?:(\w+?): )?(?:(\w+? \d+): )?(\w+?): [^:]*?not.found$/i
or $line =~
m/(?:(\w+?): )?(?:(\w+? \d+): )?\w+?: [^:]*?not.found: (\S+)$/i
or $line =~
m/(?:(\w+?): )?(?:(\w+? \d+): )?(\w+?): [^:]*?No.such.file.or.directory$/i
)
{
$error->{code} = "E404";
$error->{summary} = "Not Found - $3 is not installed";
$error->{detail} = $line;
}
elsif ( $line =~ m/^sudo: /i ) {
my $error = {};
my $runningUser = getpwuid($<);
my $cpanmbin = `which cpanm`;
my $perlbin = `which perl`;
$cpanmbin =~ s/\n//g;
$perlbin =~ s/\n//g;
$error->{code} = "E403";
$error->{summary} =
"Forbidden - " . "passwordless sudo permissions required";
$error->{detail} =
$line
. "<br /><br />"
. "You may add the following lines to /etc/sudoers.d/$runningUser:\n"
. "<pre>"
. " $runningUser ALL=(ALL) NOPASSWD:SETENV: "
. $cpanmbin . " *"
. "\n $runningUser ALL=(ALL) NOPASSWD:SETENV: "
. $perlbin
. ' - App\:\:cpanminus'
. "</pre>";
push @{ $h->{error} }, $error;
last;
}
else {
$error->{code} = "E501";
$error->{summary} = "Parsing error";
$error->{detail} = $p;
}
push @{ $h->{error} }, $error;
last;
}
}
}
else {
my $error;
$error->{code} = "E500";
$error->{summary} = "Unknown error - Missing command output";
$error->{detail} = $p;
push @{ $h->{error} }, $error;
}
# check for cpan-outdated
my $cpanoutdated = `which cpan-outdated`;
$h->{versions}{cpanoutdated} = 0
if ( $cpanoutdated =~ /^\/.+/ );
return $h;
}
sub CpanInstall($) {
my $cmd = shift;
my $h = {};
local $ENV{PATH} = __GetExtendedEnvPath();
eval {
umask 0022;
1;
};
my $p = `$cmd->{installperl}`;
if ( $p && $p ne '' ) {
$h->{success} = 0;
foreach my $line ( split /\n/, $p ) {
chomp($line);
print qq($line\n) if ( $cmd->{debug} == 1 );
if ( $line =~
/^Successfully\s+(\S+)\s+([\S]+)-(\d+(?:\.\d+(?:_\d+)?)?).*$/i )
{
my $r = $1;
my $m = $2;
my $v = $3;
$m =~ s/-/::/g;
$h->{installed}{$m}{result} = $r;
$h->{installed}{$m}{version} = $v;
$h->{success}++;
}
elsif ( $line =~
/^(\S+)\s+is\s+up\s+to\s+date.*\((\d+(?:\.\d+(?:_\d+)?)?)\).*$/i
)
{
my $m = $1;
my $v = $2;
$h->{installed}{$m}{result} = 'existing';
$h->{installed}{$m}{version} = $v;
$h->{success}++;
}
elsif ( $line =~ /^(\d+)\s+(\S+)\s+(\S+)$/i ) {
# ignore total result
}
elsif ( $line =~ /^\!\s+(.+)$/i ) {
my $error = {};
my $sum = $1;
$error->{code} = "E500";
$error->{code} = "E404" if ( $sum =~ /^Couldn't find.*/i );
$error->{summary} = $sum;
push @{ $h->{error} }, $error;
}
elsif ( $line =~
m/(?:(\w+?): )?(?:(\w+? \d+): )?(\w+?): [^:]*?not.found$/i
or $line =~
m/(?:(\w+?): )?(?:(\w+? \d+): )?\w+?: [^:]*?not.found: (\S+)$/i
or $line =~
m/(?:(\w+?): )?(?:(\w+? \d+): )?(\w+?): [^:]*?No.such.file.or.directory$/i
)
{
my $error = {};
$error->{code} = "E404";
$error->{summary} = "Not Found - $3 is not installed";
$error->{detail} = $line;
push @{ $h->{error} }, $error;
last;
}
elsif ( $line =~ m/^curl:.*Failed writing body.*$/i ) {
my $error = {};
my $runningUser = getpwuid($<);
my $cpanmbin = `which cpanm`;
my $perlbin = `which perl`;
$cpanmbin =~ s/\n//g;
$perlbin =~ s/\n//g;
$error->{code} = "E403";
$error->{summary} =
"Forbidden - " . "passwordless sudo permissions required";
$error->{detail} =
$line
. "<br /><br />"
. "You may add the following lines to /etc/sudoers.d/$runningUser:\n"
. "<pre>"
. " $runningUser ALL=(ALL) NOPASSWD:SETENV: "
. $perlbin
. ' - App\:\:cpanminus'
. "</pre>";
push @{ $h->{error} }, $error;
last;
}
elsif ( $line =~ m/^sudo: /i ) {
my $error = {};
my $runningUser = getpwuid($<);
my $cpanmbin = `which cpanm`;
$cpanmbin =~ s/\n//g;
$error->{code} = "E403";
$error->{summary} =
"Forbidden - " . "passwordless sudo permissions required";
$error->{detail} =
$line
. "<br /><br />"
. "You may add the following lines to /etc/sudoers.d/$runningUser:\n"
. "<pre>"
. " $runningUser ALL=(ALL) NOPASSWD:SETENV: "
. $cpanmbin . " *"
. "</pre>";
push @{ $h->{error} }, $error;
last;
}
else {
my $error = {};
$error->{code} = "E501";
$error->{summary} = "Parsing error";
$error->{detail} = $line;
push @{ $h->{error} }, $error;
}
}
}
else {
my $error = {};
$error->{code} = "E500";
$error->{summary} = "Unknown error - Missing command output";
$error->{detail} = $p;
push @{ $h->{error} }, $error;
}
return $h;
}
sub CpanUninstall($) {
my $cmd = shift;
my $h = {};
local $ENV{PATH} = __GetExtendedEnvPath();
my $p = `$cmd->{uninstallperl}`;
if ( $p && $p ne '' ) {
$h->{success} = 0;
foreach my $line ( split /\n/, $p ) {
chomp($line);
print qq($line\n) if ( $cmd->{debug} == 1 );
next if ( $line eq '' );
if ( $line =~ /^Successfully\s+(\S+)\s+([\S]+).*$/i ) {
$h->{uninstalled}{$2}{result} = $1;
$h->{success}++;
}
elsif ( $line =~ /^\!\s(\S+)\s+is\s+not\s+found.*$/i ) {
$h->{uninstalled}{$1}{result} = 'not installed';
$h->{success}++;
}
elsif ( $line =~ /^\!\s+(.+)$/i ) {
my $error = {};
$error->{code} = "E500";
$error->{summary} = $1;
push @{ $h->{error} }, $error;
}
elsif ( $line =~
m/(?:(\w+?): )?(?:(\w+? \d+): )?(\w+?): [^:]*?not.found$/i
or $line =~
m/(?:(\w+?): )?(?:(\w+? \d+): )?\w+?: [^:]*?not.found: (\S+)$/i
or $line =~
m/(?:(\w+?): )?(?:(\w+? \d+): )?(\w+?): [^:]*?No.such.file.or.directory$/i
)
{
my $error = {};
$error->{code} = "E404";
$error->{summary} = "Not Found - $3 is not installed";
$error->{detail} = $line;
push @{ $h->{error} }, $error;
last;
}
elsif ( $line =~ m/^sudo: /i ) {
my $error = {};
my $runningUser = getpwuid($<);
my $cpanmbin = `which cpanm`;
$cpanmbin =~ s/\n//g;
$error->{code} = "E403";
$error->{summary} =
"Forbidden - " . "passwordless sudo permissions required";
$error->{detail} =
$line
. "<br /><br />"
. "You may add the following lines to /etc/sudoers.d/$runningUser:\n"
. "<pre>"
. " $runningUser ALL=(ALL) NOPASSWD:SETENV: "
. $cpanmbin . " *"
. "</pre>";
push @{ $h->{error} }, $error;
last;
}
elsif ( $line =~ m/^\s+.*$/ ) {
# ignore lines that begin with spaces or are empty
}
else {
my $error = {};
$error->{code} = "E501";
$error->{summary} = "Parsing error";
$error->{detail} = $line;
push @{ $h->{error} }, $error;
}
}
}
else {
my $error = {};
$error->{code} = "E500";
$error->{summary} = "Unknown error - Missing command output";
$error->{detail} = $p;
push @{ $h->{error} }, $error;
}
return $h;
}
sub CpanOutdated($) {
my $cmd = shift;
my $h = {};
local $ENV{PATH} = __GetExtendedEnvPath();
my $p = `$cmd->{outdatedperl}`;
my $found = 0;
my $isConfig = 0;
my $isEnv = 0;
my $isInc = 0;
my $isOutdated = 0;
if ( $p && $p ne '' ) {
foreach my $line ( split /\n/, $p ) {
chomp($line);
print qq($line\n) if ( $cmd->{debug} == 1 );
if ( $line =~
/^cpanm (?:\([A-za-z:]+\) )?version (\d+\.\d+) (\([\w\/\.\-]+\)).*/i
)
{
$h->{versions}{cpanminus} = $1;
$found = 1;
}
elsif ( $line =~ /^perl version (\d+\.\d+) (\([\w\/\.\-]+\)).*/i ) {
$h->{versions}{perl} = $1;
}
elsif ( $line =~ /^\s+\%Config:.*$/i ) {
$isConfig = 1;
}
elsif ( $line =~ /^\s+\%ENV:.*$/i ) {
$isConfig = 0;
$isEnv = 1;
}
elsif ( $line =~ /^\s+\@INC:.*$/i ) {
$isConfig = 0;
$isEnv = 0;
$isInc = 1;
}
elsif ( $line =~ /^\@Outdated:.*$/i ) {
$isConfig = 0;
$isEnv = 0;
$isInc = 0;
$isOutdated = 1;
}
elsif ($isConfig) {
$line =~ s/^\s+//g;
my @splitted = split( /=/, $line, 2 );
$h->{versions}{Config}{ $splitted[0] } = $splitted[1];
}
elsif ($isEnv) {
$line =~ s/^\s+//g;
my @splitted = split( /=/, $line, 2 );
$h->{versions}{ENV}{ $splitted[0] } = $splitted[1];
}
elsif ($isInc) {
$line =~ s/^\s+//g;
push @{ $h->{versions}{INC} }, $line;
}
elsif ($isOutdated) {
if ( $line =~
/^([\w\:]+)\s+(\d+.\d+(?:_\d+)?)\s+(\d+.\d+(?:_\d+)?)\s+(\S+)$/
)
{
$h->{outdatedPerl}{$1}{current} = $2;
$h->{outdatedPerl}{$1}{latest} = $3;
}
else {
$found = 0;
}
}
# error
if ( !$found ) {
my $error = {};
my $runningUser = getpwuid($<);
if ( $line =~
m/(?:(\w+?): )?(?:(\w+? \d+): )?(\w+?): [^:]*?not.found$/i
or $line =~
m/(?:(\w+?): )?(?:(\w+? \d+): )?\w+?: [^:]*?not.found: (\S+)$/i
or $line =~
m/(?:(\w+?): )?(?:(\w+? \d+): )?(\w+?): [^:]*?No.such.file.or.directory$/i
)
{
$error->{code} = "E404";
$error->{summary} = "Not Found - $3 is not installed";
$error->{detail} = $line;
}
else {
$error->{code} = "E501";
$error->{summary} = "Parsing error";
$error->{detail} = $p;
}
push @{ $h->{error} }, $error;
last;
}
}
}
else {
my $error = {};
$error->{code} = "E500";
$error->{summary} = "Unknown error - Missing command output";
$error->{detail} = $p;
push @{ $h->{error} }, $error;
}
# check for cpan-outdated
my $cpanoutdated = `which cpan-outdated`;
$h->{versions}{cpanoutdated} = 0
if ( $cpanoutdated =~ /^\/.+/ );
my ($ExtUtilsInstalled) = ExtUtils::Installed->new();
my (@modules) = $ExtUtilsInstalled->modules();
foreach (@modules) {
next if ( $_ eq 'CPAN::outdated' );
my ($missing) = $ExtUtilsInstalled->validate($_);
my $version = $ExtUtilsInstalled->version($_);
$h->{listedPerl}{$_}{missing} = $missing if ($missing);
eval {
$h->{listedPerl}{$_}{version} = version->parse($version)->numify;
1;
};
if ($@) {
$h->{listedPerl}{$_}{version} = 0;
$@ = undef;
}
}
return $h;
}
####################################################
# End Childprocess
####################################################
sub PreProcessing($$) {
my ( $hash, $json ) = @_;
my $name = $hash->{NAME};
my $decode_json = eval { decode_json($json) };
if ($@) {
Log3 $name, 2, "Installer ($name) - JSON error: $@";
return;
}
Log3 $hash, 4, "Installer ($name) - JSON: $json";
if ( defined( $decode_json->{versions} )
&& defined( $decode_json->{versions}{cpanminus} ) )
{
$hash->{".fhem"}{installer}{cpanversions} =
$decode_json->{versions};
}
# safe result in hidden reading
# to restore module state after reboot
if ( $hash->{".fhem"}{installer}{cmd} eq 'outdatedPerl' ) {
delete $hash->{".fhem"}{installer}{outdatedPerlPackages};
$hash->{".fhem"}{installer}{outdatedPerlPackages} =
$decode_json->{outdatedPerl}
if ( defined( $decode_json->{outdatedPerl} ) );
delete $hash->{".fhem"}{installer}{listedPerlPackages};
$hash->{".fhem"}{installer}{listedPerlPackages} =
$decode_json->{listedPerl}
if ( defined( $decode_json->{listedPerl} ) );
readingsSingleUpdate( $hash, '.packageListPerl', $json, 0 );
}
elsif ( $hash->{".fhem"}{installer}{cmd} =~ /^install/ ) {
delete $hash->{".fhem"}{installer}{installedPerlPackages};
$hash->{".fhem"}{installer}{installedPerlPackages} = $decode_json;
readingsSingleUpdate( $hash, '.installedListPerl', $json, 0 );
}
elsif ( $hash->{".fhem"}{installer}{cmd} =~ /^uninstall/ ) {
delete $hash->{".fhem"}{installer}{uninstalledPerlPackages};
$hash->{".fhem"}{installer}{uninstalledPerlPackages} = $decode_json;
readingsSingleUpdate( $hash, '.uninstalledListPerl', $json, 0 );
}
elsif ( $hash->{".fhem"}{installer}{cmd} =~ /^update/ ) {
delete $hash->{".fhem"}{installer}{updatedPerlPackages};
$hash->{".fhem"}{installer}{updatedPerlPackages} = $decode_json;
readingsSingleUpdate( $hash, '.updatedListPerl', $json, 0 );
}
if ( defined( $decode_json->{warning} )
or defined( $decode_json->{error} ) )
{
$hash->{".fhem"}{installer}{'warnings'} = $decode_json->{warning}
if ( defined( $decode_json->{warning} ) );
$hash->{".fhem"}{installer}{errors} = $decode_json->{error}
if ( defined( $decode_json->{error} ) );
}
else {
delete $hash->{".fhem"}{installer}{'warnings'};
delete $hash->{".fhem"}{installer}{errors};
}
WriteReadings( $hash, $decode_json );
}
sub WriteReadings($$) {
my ( $hash, $decode_json ) = @_;
my $name = $hash->{NAME};
my $updatePerlDualLifeModules =
AttrVal( $name, 'updatePerlDualLifeModules', 0 );
my $updatePerlUsedModulesOnly =
AttrVal( $name, 'updatePerlUsedModulesOnly', 1 );
Log3 $hash, 4, "Installer ($name) - Write Readings";
Log3 $hash, 5, "Installer ($name) - " . Dumper $decode_json;
readingsBeginUpdate($hash);
if ( $hash->{".fhem"}{installer}{cmd} eq 'outdatedPerl' ) {
readingsBulkUpdate(
$hash,
'outdatedPerl',
(
defined( $decode_json->{listedPerl} )
? 'check completed'
: 'check failed'
)
);
$hash->{helper}{lastSync} = __ToDay();
}
my $counter = 0;
if ( $hash->{".fhem"}{installer}{cmd} eq 'outdatedPerl' ) {
foreach ( keys %{ $decode_json->{outdatedPerl} } ) {
$counter++
if (
(
$updatePerlDualLifeModules
|| !FHEM::Meta::ModuleIsPerlCore($_)
)
&& ( !$updatePerlUsedModulesOnly
|| defined( $FHEM::Meta::dependents{pkgs}{$_} ) )
);
}
readingsBulkUpdateIfChanged( $hash, 'updatesAvailablePerl', $counter );
}
readingsBulkUpdateIfChanged(
$hash,
'updateListAsJSON',
eval {
encode_json( $hash->{".fhem"}{installer}{outdatedPerlPackages} );
}
) if ( AttrVal( $name, 'updateListReading', 'none' ) ne 'none' );
my $result = 'successful';
$result = 'error' if ( defined( $hash->{".fhem"}{installer}{errors} ) );
$result = 'warning'
if ( defined( $hash->{".fhem"}{installer}{'warnings'} ) );
readingsBulkUpdate( $hash, 'installedPerl', $result )
if ( $hash->{".fhem"}{installer}{cmd} =~ /^installperl /i );
readingsBulkUpdate( $hash, 'uninstalledPerl', $result )
if ( $hash->{".fhem"}{installer}{cmd} =~ /^uninstallperl /i );
readingsBulkUpdate( $hash, 'updatedPerl', $result )
if ( $hash->{".fhem"}{installer}{cmd} =~ /^updateperl /i );
readingsBulkUpdateIfChanged( $hash, "cpanminusVersion",
$decode_json->{versions}{cpanminus} )
if ( defined( $decode_json->{versions} )
&& defined( $decode_json->{versions}{cpanminus} ) );
readingsBulkUpdateIfChanged( $hash, "perlVersion",
$decode_json->{versions}{perl} )
if ( defined( $decode_json->{versions} )
&& defined( $decode_json->{versions}{perl} ) );
if ( defined( $decode_json->{error} ) ) {
readingsBulkUpdate( $hash, 'state',
'error \'' . $hash->{".fhem"}{installer}{cmd} . '\'' );
}
elsif ( defined( $decode_json->{warning} ) ) {
readingsBulkUpdate( $hash, 'state',
'warning \'' . $hash->{".fhem"}{installer}{cmd} . '\'' );
}
else {
if ( defined( $hash->{".fhem"}{installer}{outdatedPerlPackages} ) ) {
foreach (
keys %{ $hash->{".fhem"}{installer}{outdatedPerlPackages} } )
{
$counter++
if (
(
$updatePerlDualLifeModules
|| !FHEM::Meta::ModuleIsPerlCore($_)
)
&& ( !$updatePerlUsedModulesOnly
|| defined( $FHEM::Meta::dependents{pkgs}{$_} ) )
);
}
}
readingsBulkUpdate(
$hash, 'state',
(
$counter
? 'updates available'
: 'up to date'
)
);
}
Event( $hash, "FINISH" );
readingsEndUpdate( $hash, 1 );
ProcessUpdateTimer($hash)
if ( $hash->{".fhem"}{installer}{cmd} eq 'getCpanVersion'
&& !defined( $decode_json->{error} ) );
}
sub CreateErrorList($) {
my $hash = shift;
my @ret;
my $errors;
my $html =
defined( $hash->{CL} ) && $hash->{CL}{TYPE} eq "FHEMWEB" ? 1 : 0;
$errors = $hash->{".fhem"}{installer}{errors};
# disable automatic links to FHEM devices
delete $FW_webArgs{addLinks};
my $header = '';
my $footer = '';
if ($html) {
$header = '<html>';
$footer = '</html>';
}
my $blockOpen = '';
my $tTitleOpen = '';
my $tTitleClose = '';
my $tOpen = '';
my $tCOpen = '';
my $tCClose = '';
my $tHOpen = '';
my $tHClose = '';
my $tBOpen = '';
my $tBClose = '';
my $tFOpen = '';
my $tFClose = '';
my $trOpen = '';
my $trOpenEven = '';
my $trOpenOdd = '';
my $thOpen = '';
my $thOpen2 = '';
my $thOpen3 = '';
my $tdOpen = '';
my $tdOpen2 = '';
my $tdOpen3 = '';
my $strongOpen = '';
my $strongClose = '';
my $tdClose = "\t\t\t";
my $thClose = "\t\t\t";
my $trClose = '';
my $tClose = '';
my $blockClose = '';
my $colorRed = '';
my $colorGreen = '';
my $colorClose = '';
if ($html) {
$blockOpen = '<div class="makeTable wide internals">';
$tTitleOpen = '<span class="mkTitle">';
$tTitleClose = '</span>';
$tOpen = '<table class="block wide internals wrapcolumns">';
$tCOpen = '<caption style="text-align: left; font-size: larger;">';
$tCClose = '</caption>';
$tHOpen = '<thead>';
$tHClose = '</thead>';
$tBOpen = '<tbody>';
$tBClose = '</tbody>';
$tFOpen = '<tfoot style="font-size: smaller;">';
$tFClose = '</tfoot>';
$trOpen = '<tr class="column">';
$trOpenEven = '<tr class="column even">';
$trOpenOdd = '<tr class="column odd">';
$thOpen = '<th style="text-align: left; vertical-align: top;">';
$thOpen2 =
'<th style="text-align: left; vertical-align: top;" colspan="2">';
$thOpen3 =
'<th style="text-align: left; vertical-align: top;" colspan="3">';
$tdOpen = '<td style="vertical-align: top;">';
$tdOpen2 = '<td style="vertical-align: top;" colspan="2">';
$tdOpen3 = '<td style="vertical-align: top;" colspan="3">';
$strongOpen = '<strong>';
$strongClose = '</strong>';
$tdClose = '</td>';
$thClose = '</th>';
$trClose = '</tr>';
$tClose = '</table>';
$blockClose = '</div>';
$colorRed = '<span style="color: red">';
$colorGreen = '<span style="color: green">';
$colorClose = '</span>';
}
my $space = $html ? '&nbsp;' : ' ';
my $lb = $html ? '<br />' : "\n";
my $lang = lc(
AttrVal(
$hash->{NAME}, 'language',
AttrVal( 'global', 'language', 'EN' )
)
);
my $FW_CSRF = (
defined( $defs{ $hash->{CL}{SNAME} }{CSRFTOKEN} )
? '&fwcsrf=' . $defs{ $hash->{CL}{SNAME} }{CSRFTOKEN}
: ''
);
if ( ref($errors) eq "ARRAY" && scalar @{$errors} > 0 ) {
push @ret,
$blockOpen
. $tOpen
. $tCOpen
. 'Error List'
. $tCClose
. $tHOpen
. $trOpen;
push @ret, $thOpen . 'No.' . $thClose;
push @ret, $thOpen . 'Error Code' . $thClose;
push @ret, $thOpen . 'Description' . $thClose;
push @ret, $trClose . $tHClose;
my $linecount = 1;
foreach my $error ( sort { "\L$a" cmp "\L$b" } @{$errors} ) {
my $l = $linecount % 2 == 0 ? $trOpenEven : $trOpenOdd;
$l .= $tdOpen . $linecount . $tdClose;
$l .= $tdOpen . $error->{code} . $tdClose;
$l .=
$tdOpen
. $error->{summary}
. (
defined( $error->{detail} )
&& $error->{detail} ne ''
? $lb
. $lb
. $strongOpen
. 'Detail:'
. $strongClose
. $lb
. $error->{detail}
: ''
) . $tdClose;
$l .= $trClose;
push @ret, $l;
$linecount++;
}
push @ret, $tClose . $blockClose;
}
else {
push @ret, 'No errors occured during last command.';
}
return $header . join( "\n", @ret ) . $footer;
}
sub CreateInstalledPerlList($$) {
my ( $hash, $getCmd ) = @_;
my @ret;
my $packages;
my $html =
defined( $hash->{CL} ) && $hash->{CL}{TYPE} eq "FHEMWEB" ? 1 : 0;
$packages = $hash->{".fhem"}{installer}{listedPerlPackages};
# disable automatic links to FHEM devices
delete $FW_webArgs{addLinks};
my $header = '';
my $footer = '';
if ($html) {
$header = '<html>';
$footer = '</html>';
}
my $blockOpen = '';
my $tTitleOpen = '';
my $tTitleClose = '';
my $tOpen = '';
my $tCOpen = '';
my $tCClose = '';
my $tHOpen = '';
my $tHClose = '';
my $tBOpen = '';
my $tBClose = '';
my $tFOpen = '';
my $tFClose = '';
my $trOpen = '';
my $trOpenEven = '';
my $trOpenOdd = '';
my $thOpen = '';
my $thOpen2 = '';
my $thOpen3 = '';
my $tdOpen = '';
my $tdOpen2 = '';
my $tdOpen3 = '';
my $strongOpen = '';
my $strongClose = '';
my $tdClose = "\t\t\t";
my $thClose = "\t\t\t";
my $trClose = '';
my $tClose = '';
my $blockClose = '';
my $colorRed = '';
my $colorGreen = '';
my $colorClose = '';
if ($html) {
$blockOpen = '<div class="makeTable wide internals">';
$tTitleOpen = '<span class="mkTitle">';
$tTitleClose = '</span>';
$tOpen = '<table class="block wide internals wrapcolumns">';
$tCOpen = '<caption style="text-align: left; font-size: larger;">';
$tCClose = '</caption>';
$tHOpen = '<thead>';
$tHClose = '</thead>';
$tBOpen = '<tbody>';
$tBClose = '</tbody>';
$tFOpen = '<tfoot style="font-size: smaller;">';
$tFClose = '</tfoot>';
$trOpen = '<tr class="column">';
$trOpenEven = '<tr class="column even">';
$trOpenOdd = '<tr class="column odd">';
$thOpen = '<th style="text-align: left; vertical-align: top;">';
$thOpen2 =
'<th style="text-align: left; vertical-align: top;" colspan="2">';
$thOpen3 =
'<th style="text-align: left; vertical-align: top;" colspan="3">';
$tdOpen = '<td style="vertical-align: top;">';
$tdOpen2 = '<td style="vertical-align: top;" colspan="2">';
$tdOpen3 = '<td style="vertical-align: top;" colspan="3">';
$strongOpen = '<strong>';
$strongClose = '</strong>';
$tdClose = '</td>';
$thClose = '</th>';
$trClose = '</tr>';
$tClose = '</table>';
$blockClose = '</div>';
$colorRed = '<span style="color: red">';
$colorGreen = '<span style="color: green">';
$colorClose = '</span>';
}
my $space = $html ? '&nbsp;' : ' ';
my $lb = $html ? '<br />' : "\n";
my $lang = lc(
AttrVal(
$hash->{NAME}, 'language',
AttrVal( 'global', 'language', 'EN' )
)
);
my $FW_CSRF = (
defined( $defs{ $hash->{CL}{SNAME} }{CSRFTOKEN} )
? '&fwcsrf=' . $defs{ $hash->{CL}{SNAME} }{CSRFTOKEN}
: ''
);
if ( ref($packages) eq "HASH" && scalar keys %{$packages} > 0 ) {
push @ret,
$blockOpen
. $tOpen
. $tCOpen
. 'Installed Perl Packages'
. $tCClose
. $tHOpen
. $trOpen;
push @ret, $thOpen . 'Name' . $thClose;
push @ret, $thOpen . 'Version' . $thClose;
push @ret, $trClose . $tHClose;
my $linecount = 1;
foreach my $package ( sort { "\L$a" cmp "\L$b" } keys %{$packages} ) {
next if ( $package eq "undefined" );
my $l = $linecount % 2 == 0 ? $trOpenEven : $trOpenOdd;
$l .= $tdOpen . $package . $tdClose;
$l .= $tdOpen
. (
defined( $packages->{$package}{version} )
&& $packages->{$package}{version}
? $packages->{$package}{version}
: '?'
) . $tdClose;
$l .= $trClose;
push @ret, $l;
$linecount++;
}
push @ret, $tClose . $blockClose;
}
else {
push @ret, 'No installed Perl packages found.';
}
return $header . join( "\n", @ret ) . $footer;
}
sub CreateOutdatedPerlList($$) {
my ( $hash, $getCmd ) = @_;
my @ret;
my $packages;
my $html =
defined( $hash->{CL} ) && $hash->{CL}{TYPE} eq "FHEMWEB" ? 1 : 0;
$packages = $hash->{".fhem"}{installer}{outdatedPerlPackages};
# disable automatic links to FHEM devices
delete $FW_webArgs{addLinks};
my $header = '';
my $footer = '';
if ($html) {
$header = '<html>';
$footer = '</html>';
}
my $blockOpen = '';
my $tTitleOpen = '';
my $tTitleClose = '';
my $tOpen = '';
my $tCOpen = '';
my $tCClose = '';
my $tHOpen = '';
my $tHClose = '';
my $tBOpen = '';
my $tBClose = '';
my $tFOpen = '';
my $tFClose = '';
my $trOpen = '';
my $trOpenEven = '';
my $trOpenOdd = '';
my $thOpen = '';
my $thOpen2 = '';
my $thOpen3 = '';
my $tdOpen = '';
my $tdOpen2 = '';
my $tdOpen3 = '';
my $strongOpen = '';
my $strongClose = '';
my $tdClose = "\t\t\t";
my $thClose = "\t\t\t";
my $trClose = '';
my $tClose = '';
my $blockClose = '';
my $colorRed = '';
my $colorGreen = '';
my $colorClose = '';
if ($html) {
$blockOpen = '<div class="makeTable wide internals">';
$tTitleOpen = '<span class="mkTitle">';
$tTitleClose = '</span>';
$tOpen = '<table class="block wide internals wrapcolumns">';
$tCOpen = '<caption style="text-align: left; font-size: larger;">';
$tCClose = '</caption>';
$tHOpen = '<thead>';
$tHClose = '</thead>';
$tBOpen = '<tbody>';
$tBClose = '</tbody>';
$tFOpen = '<tfoot style="font-size: smaller;">';
$tFClose = '</tfoot>';
$trOpen = '<tr class="column">';
$trOpenEven = '<tr class="column even">';
$trOpenOdd = '<tr class="column odd">';
$thOpen = '<th style="text-align: left; vertical-align: top;">';
$thOpen2 =
'<th style="text-align: left; vertical-align: top;" colspan="2">';
$thOpen3 =
'<th style="text-align: left; vertical-align: top;" colspan="3">';
$tdOpen = '<td style="vertical-align: top;">';
$tdOpen2 = '<td style="vertical-align: top;" colspan="2">';
$tdOpen3 = '<td style="vertical-align: top;" colspan="3">';
$strongOpen = '<strong>';
$strongClose = '</strong>';
$tdClose = '</td>';
$thClose = '</th>';
$trClose = '</tr>';
$tClose = '</table>';
$blockClose = '</div>';
$colorRed = '<span style="color: red">';
$colorGreen = '<span style="color: green">';
$colorClose = '</span>';
}
my $space = $html ? '&nbsp;' : ' ';
my $lb = $html ? '<br />' : "\n";
my $lang = lc(
AttrVal(
$hash->{NAME}, 'language',
AttrVal( 'global', 'language', 'EN' )
)
);
my $FW_CSRF = (
defined( $defs{ $hash->{CL}{SNAME} }{CSRFTOKEN} )
? '&fwcsrf=' . $defs{ $hash->{CL}{SNAME} }{CSRFTOKEN}
: ''
);
if ( ref($packages) eq "HASH" ) {
my $updatePerlDualLifeModules =
AttrVal( $hash->{NAME}, 'updatePerlDualLifeModules', 0 );
my $updatePerlUsedModulesOnly =
AttrVal( $hash->{NAME}, 'updatePerlUsedModulesOnly', 1 );
my $counter = 0;
foreach ( keys %{$packages} ) {
$counter++
if (
(
$updatePerlDualLifeModules
|| !FHEM::Meta::ModuleIsPerlCore($_)
)
&& ( !$updatePerlUsedModulesOnly
|| defined( $FHEM::Meta::dependents{pkgs}{$_} ) )
);
}
if ($counter) {
push @ret,
$blockOpen
. $tOpen
. $tCOpen
. 'Outdated Perl Packages'
. $tCClose
. $tHOpen
. $trOpen;
push @ret, $thOpen . 'Name' . $thClose;
push @ret, $thOpen . 'Current Version' . $thClose;
push @ret, $thOpen . 'Latest Version' . $thClose;
push @ret, $trClose . $tHClose;
my $linecount = 1;
foreach my $package ( sort { "\L$a" cmp "\L$b" } keys %{$packages} )
{
next if ( $package eq "undefined" );
next
if (
(
!$updatePerlDualLifeModules
&& FHEM::Meta::ModuleIsPerlCore($package)
)
|| ( $updatePerlUsedModulesOnly
&& !defined( $FHEM::Meta::dependents{pkgs}{$package} ) )
);
my $l = $linecount % 2 == 0 ? $trOpenEven : $trOpenOdd;
$l .= $tdOpen . $package . $tdClose;
$l .= $tdOpen
. (
defined( $packages->{$package}{current} )
? $packages->{$package}{current}
: '?'
) . $tdClose;
$l .= $tdOpen
. (
defined( $packages->{$package}{latest} )
? $packages->{$package}{latest}
: '?'
) . $tdClose;
$l .= $trClose;
push @ret, $l;
$linecount++;
}
}
else {
push @ret, 'No outdated Perl packages found.'
}
}
return $header . join( "\n", @ret ) . $footer;
}
sub CreatePrereqsList {
my $hash = shift;
my $getCmd = shift;
my $cfgfile = shift;
my $mode =
( $cfgfile && $cfgfile eq '1' ? 'all' : ( $cfgfile ? 'file' : 'live' ) );
$mode = 'list' if ( $cfgfile && defined( $modules{$cfgfile} ) );
my @defined;
if ( $mode eq 'live' || $mode eq 'all' ) {
foreach ( keys %modules ) {
next
unless ( $mode eq 'all' || defined( $modules{$_}{LOADED} ) );
push @defined, $_;
}
}
elsif ( $mode eq 'file' ) {
@defined = __GetDefinedModulesFromFile($cfgfile);
return
'File '
. $cfgfile
. ' does not seem to contain any FHEM device configuration'
unless ( @defined > 0 );
}
elsif ( $mode eq 'list' ) {
@defined = @_;
unshift @defined, $cfgfile;
}
# disable automatic links to FHEM devices
delete $FW_webArgs{addLinks};
my @ret;
my $html =
defined( $hash->{CL} ) && $hash->{CL}{TYPE} eq "FHEMWEB" ? 1 : 0;
my $header = '';
my $footer = '';
if ($html) {
$header = '<html>';
$footer = '</html>';
}
my $blockOpen = '';
my $tTitleOpen = '';
my $tTitleClose = '';
my $tOpen = '';
my $tCOpen = '';
my $tCClose = '';
my $tHOpen = '';
my $tHClose = '';
my $tBOpen = '';
my $tBClose = '';
my $tFOpen = '';
my $tFClose = '';
my $trOpen = '';
my $trOpenEven = '';
my $trOpenOdd = '';
my $thOpen = '';
my $thOpen2 = '';
my $thOpen3 = '';
my $tdOpen = '';
my $tdOpen1 = '';
my $tdOpen2 = '';
my $tdOpen3 = '';
my $tdOpen4 = '';
my $strongOpen = '';
my $strongClose = '';
my $tdClose = "\t\t\t";
my $thClose = "\t\t\t";
my $trClose = '';
my $tClose = '';
my $blockClose = '';
my $colorRed = '';
my $colorGreen = '';
my $colorClose = '';
if ($html) {
$blockOpen = '<div class="makeTable wide internals">';
$tTitleOpen = '<span class="mkTitle">';
$tTitleClose = '</span>';
$tOpen = '<table class="block wide internals wrapcolumns">';
$tCOpen = '<caption style="text-align: left; font-size: larger;">';
$tCClose = '</caption>';
$tHOpen = '<thead>';
$tHClose = '</thead>';
$tBOpen = '<tbody>';
$tBClose = '</tbody>';
$tFOpen = '<tfoot style="font-size: smaller;">';
$tFClose = '</tfoot>';
$trOpen = '<tr class="column">';
$trOpenEven = '<tr class="column even">';
$trOpenOdd = '<tr class="column odd">';
$thOpen = '<th style="text-align: left; vertical-align: top;">';
$thOpen2 =
'<th style="text-align: left; vertical-align: top;" colspan="2">';
$thOpen3 =
'<th style="text-align: left; vertical-align: top;" colspan="3">';
$tdOpen = '<td style="vertical-align: top;">';
$tdOpen1 = '<td style="vertical-align: top;" colspan="1">';
$tdOpen2 = '<td style="vertical-align: top;" colspan="2">';
$tdOpen3 = '<td style="vertical-align: top;" colspan="3">';
$tdOpen4 = '<td style="vertical-align: top;" colspan="4">';
$strongOpen = '<strong>';
$strongClose = '</strong>';
$tdClose = '</td>';
$thClose = '</th>';
$trClose = '</tr>';
$tClose = '</table>';
$blockClose = '</div>';
$colorRed = '<span style="color: red">';
$colorGreen = '<span style="color: green">';
$colorClose = '</span>';
}
my $space = $html ? '&nbsp;' : ' ';
my $lb = $html ? '<br />' : "\n";
my $lang = lc(
AttrVal(
$hash->{NAME}, 'language',
AttrVal( 'global', 'language', 'EN' )
)
);
my $FW_CSRF = (
defined( $defs{ $hash->{CL}{SNAME} }{CSRFTOKEN} )
? '&fwcsrf=' . $defs{ $hash->{CL}{SNAME} }{CSRFTOKEN}
: ''
);
########
# Getting Perl prereqs
LoadInstallStatusPerl( $defined[0] eq '1' ? 1 : \@defined );
my $found = 0;
my $foundRequired = 0;
my $foundRecommended = 0;
my $foundSuggested = 0;
my $foundRequiredPerl = 0;
my $foundRecommendedPerl = 0;
my $foundSuggestedPerl = 0;
my $foundRequiredNodejs = 0;
my $foundRecommendedNodejs = 0;
my $foundSuggestedNodejs = 0;
my $foundRequiredPython = 0;
my $foundRecommendedPython = 0;
my $foundSuggestedPython = 0;
# Display prereqs
foreach my $mAttr (qw(required recommended suggested)) {
foreach my $area (qw(Perl Node.js Python)) {
next
unless ( defined( $pkgStatus{$mAttr} )
&& defined( $pkgStatus{$mAttr}{$area} )
&& keys %{ $pkgStatus{$mAttr}{$area} } > 0 );
my $linecount = 1;
my $importance = ucfirst($mAttr);
foreach my $item (
sort { "\L$a" cmp "\L$b" }
keys %{ $pkgStatus{$mAttr}{$area} }
)
{
my $linkmod = '';
my $inScope = 0;
foreach my $modName ( sort { "\L$a" cmp "\L$b" }
@{ $pkgStatus{$mAttr}{$area}{$item}{modules} } )
{
# check if this package is used by any
# module that is in install scope
if ( grep ( /^$modName$/, @defined ) ) {
$inScope = 1;
}
$linkmod .= ', ' unless ( $linkmod eq '' );
if ($html) {
$linkmod .=
'<a href="?cmd=get '
. $hash->{NAME}
. ' showModuleInfo '
. $modName
. $FW_CSRF . '">'
. ( $modName eq 'Global' ? 'FHEM' : $modName )
. '</a>';
}
else {
$linkmod .=
( $modName eq 'Global' ? 'FHEM' : $modName );
}
}
next unless ($inScope);
$found++;
$foundRequired++ if ( $mAttr eq 'required' );
$foundRecommended++ if ( $mAttr eq 'recommended' );
$foundSuggested++ if ( $mAttr eq 'suggested' );
$foundRequiredPerl++
if ( $area eq 'Perl' && $mAttr eq 'required' );
$foundRecommendedPerl++
if ( $area eq 'Perl' && $mAttr eq 'recommended' );
$foundSuggestedPerl++
if ( $area eq 'Perl' && $mAttr eq 'suggested' );
$foundRequiredNodejs++
if ( $area eq 'Node.js' && $mAttr eq 'required' );
$foundRecommendedNodejs++
if ( $area eq 'Node.js' && $mAttr eq 'recommended' );
$foundSuggestedNodejs++
if ( $area eq 'Node.js' && $mAttr eq 'suggested' );
$foundRequiredPython++
if ( $area eq 'Python' && $mAttr eq 'required' );
$foundRecommendedPython++
if ( $area eq 'Python' && $mAttr eq 'recommended' );
$foundSuggestedPython++
if ( $area eq 'Python' && $mAttr eq 'suggested' );
my $l = $linecount % 2 == 0 ? $trOpenEven : $trOpenOdd;
my $linkitem = $item;
$linkitem =
'<a href="https://metacpan.org/pod/'
. $item
. '" target="_blank">'
. $item . '</a>'
if ($html);
my $action =
'<div class="detLink installerAction"><a href="?cmd=set '
. $hash->{NAME}
. ' installPerl '
. $item . ' '
. $FW_CSRF
. '" title="Click here to trigger installation">Install</a></div>';
$l .=
$tdOpen
. $linkitem
. (
$pkgStatus{$mAttr}{$area}{$item}{status} eq 'outdated'
? ' (wanted version: '
. $pkgStatus{$mAttr}{$area}{$item}{version} . ')'
: ''
) . $tdClose;
$l .= $tdOpen . $area . $tdClose;
$l .= $tdOpen . $linkmod . $tdClose;
$l .= $tdOpen . $action . $tdClose if ($html);
$l .= $trClose;
if ( $linecount == 1 ) {
push @ret,
$trOpen
. $tdOpen
. (
$html
? '<a name="prereqResult' . $importance . '"></a>'
: ''
)
. $blockOpen
. $tOpen
. $tCOpen
. $importance
. $tCClose;
push @ret, $tHOpen . $trOpen;
push @ret, $thOpen . 'Item' . $thClose;
push @ret, $thOpen . 'Type' . $thClose;
push @ret, $thOpen . 'Used by' . $thClose;
push @ret, $thOpen . 'Action' . $thClose if ($html);
push @ret, $trClose . $tHClose . $tBOpen;
}
push @ret, $l;
$linecount++;
}
if ( $linecount > 1 ) {
my $action =
'<div class="detLink installerAction"><a href="?cmd=set '
. $hash->{NAME}
. ' install'
. ucfirst($area) . ' all-'
. $mAttr
. $FW_CSRF
. '" title="Click here to trigger installation">Install '
. $mAttr . ' '
. ucfirst($area)
. '</a></div>';
push @ret,
( $linecount % 2 == 0 ? $trOpenEven : $trOpenOdd )
. $tdOpen3
. $tdClose
. $tdOpen1
. $action
. $tdClose
. $trClose
if ($html);
push @ret, $tBClose;
my $descr =
'Required dependencies '
. $strongOpen . 'must'
. $strongClose
. ' be installed for the listed FHEM modules to work.';
$descr =
'Recommended dependencies are '
. $strongOpen
. 'strongly encouraged'
. $strongClose
. ' and should be installed for full functionality of the listed FHEM modules, except in resource constrained environments.'
if ( $importance eq 'Recommended' );
$descr =
'Suggested dependencies are '
. $strongOpen
. 'optional'
. $strongClose
. ', but are suggested for enhanced operation of the listed FHEM modules.'
if ( $importance eq 'Suggested' );
push @ret,
$tFOpen . ( $html ? $tdOpen4 : $tdOpen3 ) . $descr . $tFClose;
push @ret, $tClose . $blockClose . $tdClose . $trClose;
}
}
}
if ($found) {
push @ret, $tBClose;
if ( defined( $pkgStatus{Perl}{analyzed} ) ) {
push @ret,
$tFOpen
. $trOpen
. $tdOpen
. $strongOpen . 'Hint:'
. $strongClose
. ' Some of the FHEM modules in use do not provide Perl prerequisites from its metadata.'
. $lb;
if ( $pkgStatus{Perl}{analyzed} == 1 ) {
push @ret,
'This check is based on automatic source code analysis and can be incorrect.'
. ' Suggested Perl items may still be required if the module author had decided to implement some own dependency and/or error handling like returning an informative message instead of the original Perl error message.';
}
elsif ( $pkgStatus{Perl}{analyzed} == 2 ) {
push @ret,
'This check may be incomplete until you '
. (
$html
? '<a href="?cmd=set '
. $hash->{NAME}
. ' installPerl Perl::PrereqScanner::NotQuiteLite'
. $FW_CSRF
. '">click here to install Perl::PrereqScanner::NotQuiteLite</a> '
: 'install Perl::PrereqScanner::NotQuiteLite '
) . 'for automatic source code analysis.';
}
push @ret, $tdClose . $trClose . $tFClose;
}
unshift @ret, $lb . $lb . $tdClose . $trClose;
unshift @ret,
$lb
. $space
. $space
. ( $html ? '<a href="#prereqResultSuggested">' : '' )
. $foundSuggested
. ' suggested '
. ( $foundSuggested > 1 ? 'items' : 'item' )
. ( $html ? '</a>' : '' )
if ($foundSuggested);
unshift @ret,
$lb
. $space
. $space
. ( $html ? '<a href="#prereqResultRecommended">' : '' )
. $foundRecommended
. ' recommended '
. ( $foundRecommended > 1 ? 'items' : 'item' )
. ( $html ? '</a>' : '' )
if ($foundRecommended);
unshift @ret,
$lb
. $space
. $space
. ( $html ? '<a href="#prereqResultRequired">' : '' )
. $foundRequired
. ' required '
. ( $foundRequired > 1 ? 'items' : 'item' )
. ( $html ? '</a>' : '' )
if ($foundRequired);
unshift @ret,
$found
. ' total missing '
. ( $found > 1 ? 'prerequisites:' : 'prerequisite:' );
unshift @ret, $blockOpen . $blockClose;
unshift @ret, $tBOpen . $trOpen . $tdOpen;
}
else {
my @hooray = (
'hooray', 'hurray', 'phew', 'woop woop',
'woopee', 'wow', 'yay', 'yippie',
);
my $x = 0 + int( rand( scalar @hooray + 1 - 0 ) );
unshift @ret,
$tBOpen
. $trOpen
. $tdOpen
. $lb
. ucfirst( $hooray[$x] )
. '! All prerequisites are met.'
. ( $html ? ' 🥳' : '' )
. $lb
. $lb
. $tdClose
. $trClose
. $tBClose;
}
push @ret, $tClose . $blockClose;
unshift @ret,
$blockOpen
. $blockClose
. ( $html ? '<a name="prereqResultTOP"></a>' : '' )
. $blockOpen
. $tTitleOpen
. ( $mode eq 'live' ? 'Live ' : '' )
. 'System Prerequisites Check'
. $tTitleClose
. $tOpen;
return $header . join( "\n", @ret ) . $footer;
}
sub CreateSearchList ($$@) {
my $hash = shift;
my $getCmd = shift;
my $search = join( '\s*', @_ );
$search = '.+' unless ($search);
# disable automatic links to FHEM devices
delete $FW_webArgs{addLinks};
my @ret;
my $html =
defined( $hash->{CL} ) && $hash->{CL}{TYPE} eq "FHEMWEB" ? 1 : 0;
my $FW_CSRF = (
defined( $defs{ $hash->{CL}{SNAME} }{CSRFTOKEN} )
? '&fwcsrf=' . $defs{ $hash->{CL}{SNAME} }{CSRFTOKEN}
: ''
);
my $FW_CSRF_input =
defined( $defs{ $hash->{CL}{SNAME} }{CSRFTOKEN} )
? '<input type="hidden" name="fwcsrf" value="'
. $defs{ $hash->{CL}{SNAME} }{CSRFTOKEN} . '">'
: '';
my $header = '';
my $footer = '';
if ($html) {
$header = '<html>';
$footer = '</html>';
}
my $blockOpen = '';
my $tTitleOpen = '';
my $tTitleClose = '';
my $tOpen = '';
my $tCOpen = '';
my $tCClose = '';
my $tHOpen = '';
my $tHClose = '';
my $tBOpen = '';
my $tBClose = '';
my $tFOpen = '';
my $tFClose = '';
my $trOpen = '';
my $trOpenEven = '';
my $trOpenOdd = '';
my $thOpen = '';
my $thOpen2 = '';
my $thOpen3 = '';
my $tdOpen = '';
my $tdOpen2 = '';
my $tdOpen3 = '';
my $strongOpen = '';
my $strongClose = '';
my $tdClose = "\t\t\t";
my $thClose = "\t\t\t";
my $trClose = '';
my $tClose = '';
my $blockClose = '';
my $colorRed = '';
my $colorGreen = '';
my $colorClose = '';
if ($html) {
$blockOpen = '<div class="makeTable wide internals">';
$tTitleOpen = '<span class="mkTitle">';
$tTitleClose = '</span>';
$tOpen = '<table class="block wide internals wrapcolumns">';
$tCOpen = '<caption style="text-align: left; font-size: larger;">';
$tCClose = '</caption>';
$tHOpen = '<thead>';
$tHClose = '</thead>';
$tBOpen = '<tbody>';
$tBClose = '</tbody>';
$tFOpen = '<tfoot style="font-size: smaller;">';
$tFClose = '</tfoot>';
$trOpen = '<tr class="column">';
$trOpenEven = '<tr class="column even">';
$trOpenOdd = '<tr class="column odd">';
$thOpen = '<th style="text-align: left; vertical-align: top;">';
$thOpen2 =
'<th style="text-align: left; vertical-align: top;" colspan="2">';
$thOpen3 =
'<th style="text-align: left; vertical-align: top;" colspan="3">';
$tdOpen = '<td style="vertical-align: top;">';
$tdOpen2 = '<td style="vertical-align: top;" colspan="2">';
$tdOpen3 = '<td style="vertical-align: top;" colspan="3">';
$strongOpen = '<strong>';
$strongClose = '</strong>';
$tdClose = '</td>';
$thClose = '</td>';
$trClose = '</tr>';
$tClose = '</table>';
$blockClose = '</div>';
$colorRed = '<span style="color: red">';
$colorGreen = '<span style="color: green">';
$colorClose = '</span>';
}
my $space = $html ? '&nbsp;' : ' ';
my $lb = $html ? '<br />' : "\n";
my $lang = lc(
AttrVal(
$hash->{NAME}, 'language',
AttrVal( 'global', 'language', 'EN' )
)
);
# Add search input
$header .=
'<div class="wide">'
. '<form id="fhemsearch" method="get" action="?" onsubmit="cmd.value = \'get '
. $hash->{NAME}
. ' search \'+ q.value" style="float: left; margin-right: 2em;">'
. $FW_CSRF_input
. '<input type="hidden" name="cmd" value="">'
. '<label for="q" style="margin-right: 0.5em;">Search:</label>'
. '<input type="text" name="q" id="q" value="'
. $search
. '" autocorrect="off" autocapitalize="off">'
. '</form>'
. '<div class="detLink installerBack" style="float: right"><a href="?detail='
. $hash->{NAME}
. '">&larr; back to FHEM Installer</a></div>'
. '</div>';
my $found = 0;
# search for matching device
my $foundDevices = 0;
my $linecount = 1;
foreach my $device ( sort { "\L$a" cmp "\L$b" } keys %defs ) {
next
unless ( defined( $defs{$device}{TYPE} )
&& !defined( $defs{$device}{TEMPORARY} )
&& defined( $modules{ $defs{$device}{TYPE} } ) );
if ( $device =~ m/^.*$search.*$/i ) {
unless ($foundDevices) {
push @ret,
( $html ? '<a name="searchResultDevices"></a>' : '' )
. $blockOpen
. $tOpen
. $tCOpen
. 'Devices'
. $tCClose
. $tHOpen
. $trOpen;
push @ret, $thOpen . 'Device Name' . $thClose;
push @ret, $thOpen . 'Device Type' . $thClose;
push @ret, $thOpen . 'Device State' . $thClose;
push @ret, $trClose . $tHClose;
}
$found++;
$foundDevices++;
my $l = $linecount % 2 == 0 ? $trOpenEven : $trOpenOdd;
FHEM::Meta::Load( $defs{$device}{TYPE} );
my $linkDev = $device;
$linkDev =
'<a href="?detail='
. $device
. $FW_CSRF . '">'
. $device . '</a>'
if ($html);
my $linkMod = $defs{$device}{TYPE};
$linkMod =
'<a href="?cmd=get '
. $hash->{NAME}
. ' showModuleInfo '
. $defs{$device}{TYPE}
. $FW_CSRF . '">'
. $defs{$device}{TYPE} . '</a>'
if ($html);
$l .= $tdOpen . $linkDev . $tdClose;
$l .= $tdOpen . $linkMod . $tdClose;
$l .= $tdOpen
. (
defined( $defs{$device}{STATE} )
? $defs{$device}{STATE}
: ''
) . $tdClose;
$l .= $trClose;
push @ret, $l;
$linecount++;
}
}
push @ret, $tClose . $blockClose if ($foundDevices);
# search for matching module
my $foundModules = 0;
$linecount = 1;
foreach my $module ( sort { "\L$a" cmp "\L$b" } keys %modules ) {
if ( $module =~ m/^.*$search.*$/i ) {
unless ($foundModules) {
push @ret,
( $html ? '<a name="searchResultModules"></a>' : '' )
. $blockOpen
. $tOpen
. $tCOpen
. 'Modules'
. $tCClose
. $tHOpen
. $trOpen;
push @ret, $thOpen . 'Module Name' . $thClose;
push @ret, $thOpen . 'Abstract' . $thClose;
push @ret, $trClose . $tHClose;
}
$found++;
$foundModules++;
my $l = $linecount % 2 == 0 ? $trOpenEven : $trOpenOdd;
FHEM::Meta::Load($module);
my $abstract = '';
$abstract = $modules{$module}{META}{abstract}
if ( defined( $modules{$module}{META} )
&& defined( $modules{$module}{META}{abstract} ) );
my $link = $module;
$link =
'<a href="?cmd=get '
. $hash->{NAME}
. ' showModuleInfo '
. $module
. $FW_CSRF . '">'
. $module . '</a>'
if ($html);
$l .= $tdOpen . $link . $tdClose;
$l .= $tdOpen . ( $abstract eq 'n/a' ? '' : $abstract ) . $tdClose;
$l .= $trClose;
push @ret, $l;
$linecount++;
}
}
push @ret, $tClose . $blockClose if ($foundModules);
# search for matching module
my $foundPackages = 0;
$linecount = 1;
foreach my $package ( sort { "\L$a" cmp "\L$b" } keys %packages ) {
if ( $package =~ m/^.*$search.*$/i ) {
unless ($foundPackages) {
push @ret,
( $html ? '<a name="searchResultPackages"></a>' : '' )
. $blockOpen
. $tOpen
. $tCOpen
. 'Packages'
. $tCClose
. $tHOpen
. $trOpen;
push @ret, $thOpen . 'Package Name' . $thClose;
push @ret, $thOpen . 'Abstract' . $thClose;
push @ret, $trClose . $tHClose;
}
$found++;
$foundPackages++;
my $l = $linecount % 2 == 0 ? $trOpenEven : $trOpenOdd;
FHEM::Meta::Load($package);
my $abstract = '';
$abstract = $packages{$package}{META}{abstract}
if ( defined( $packages{$package}{META} )
&& defined( $packages{$package}{META}{abstract} ) );
my $link = $package;
$link =
'<a href="?cmd=get '
. $hash->{NAME}
. ' showPackageInfo '
. $package
. $FW_CSRF . '">'
. $package . '</a>'
if ($html);
$l .= $tdOpen . $link . $tdClose;
$l .= $tdOpen . ( $abstract eq 'n/a' ? '' : $abstract ) . $tdClose;
$l .= $trClose;
push @ret, $l;
$linecount++;
}
}
push @ret, $tClose . $blockClose if ($foundPackages);
# search for matching keyword
my $foundKeywords = 0;
$linecount = 1;
foreach
my $keyword ( sort { "\L$a" cmp "\L$b" } keys %FHEM::Meta::keywords )
{
if ( $keyword =~ m/^.*$search.*$/i ) {
push @ret, '<a name="searchResultKeywords"></a>'
unless ($foundKeywords);
$found++;
$foundKeywords++;
my $descr = FHEM::Meta::GetKeywordDesc( $keyword, $lang );
push @ret, $blockOpen . $tOpen;
if ($html) {
push @ret,
'<caption style="text-align: left; font-size: larger;"'
. ( $descr ne '' ? ' title="' . $descr . '"' : '' )
. '># '
. $keyword
. $tCClose;
}
else {
push @ret, '# ' . $keyword;
}
my @mAttrs = qw(
modules
packages
);
push @ret, $tHOpen . $trOpen;
push @ret, $thOpen . 'Name' . $thClose;
push @ret, $thOpen . 'Type' . $thClose;
push @ret, $thOpen . 'Abstract' . $thClose;
push @ret, $trClose . $tHClose;
foreach my $mAttr (@mAttrs) {
next
unless ( defined( $FHEM::Meta::keywords{$keyword}{$mAttr} )
&& @{ $FHEM::Meta::keywords{$keyword}{$mAttr} } > 0 );
foreach my $item ( sort { "\L$a" cmp "\L$b" }
@{ $FHEM::Meta::keywords{$keyword}{$mAttr} } )
{
my $l = $linecount % 2 == 0 ? $trOpenEven : $trOpenOdd;
my $type = $mAttr;
$type = 'Module' if ( $mAttr eq 'modules' );
$type = 'Package' if ( $mAttr eq 'packages' );
FHEM::Meta::Load($item);
my $abstract = '';
$abstract = $modules{$item}{META}{abstract}
if ( defined( $modules{$item} )
&& defined( $modules{$item}{META} )
&& defined( $modules{$item}{META}{abstract} ) );
my $link = $item;
$link =
'<a href="?cmd=get '
. $hash->{NAME}
. (
$type eq 'Module'
? ' showModuleInfo '
: ' showPackageInfo '
)
. $item
. $FW_CSRF . '">'
. $item . '</a>'
if ($html);
$l .= $tdOpen . $link . $tdClose;
$l .= $tdOpen . $type . $tdClose;
$l .=
$tdOpen
. ( $abstract eq 'n/a' ? '' : $abstract )
. $tdClose;
$l .= $trClose;
push @ret, $l;
$linecount++;
}
}
push @ret, $tClose . $blockClose;
}
}
# search for matching maintainer
my $foundMaintainers = 0;
$linecount = 1;
foreach my $maintainer (
sort { "\L$a" cmp "\L$b" }
keys %FHEM::Meta::maintainers
)
{
if ( $maintainer =~ m/^.*$search.*$/i ) {
unless ($foundMaintainers) {
push @ret,
$blockOpen
. $tOpen
. $tCOpen
. ( $html ? '<a name="searchResultMaintainers"></a>' : '' )
. 'Authors & Maintainers'
. $tCClose
. $tHOpen
. $trOpen;
push @ret, $thOpen . 'Name' . $thClose;
push @ret, $thOpen . 'Modules' . $thClose;
push @ret, $thOpen . 'Packages' . $thClose;
push @ret, $trClose . $tHClose;
}
$found++;
$foundMaintainers++;
my $l = $linecount % 2 == 0 ? $trOpenEven : $trOpenOdd;
my $mods = '';
if ( defined( $FHEM::Meta::maintainers{$maintainer}{modules} ) ) {
my $counter = 0;
foreach my $mod ( sort { "\L$a" cmp "\L$b" }
@{ $FHEM::Meta::maintainers{$maintainer}{modules} } )
{
if ($html) {
$mods .= '<br />' if ($counter);
$mods .=
'<a href="?cmd=get '
. $hash->{NAME}
. ' showModuleInfo '
. $mod
. $FW_CSRF . '">'
. $mod . '</a>';
}
else {
$mods .= "\n" unless ($counter);
$mods .= $mod;
}
$counter++;
}
}
my $pkgs = '';
if ( defined( $FHEM::Meta::maintainers{$maintainer}{packages} ) ) {
my $counter = 0;
foreach my $pkg ( sort { "\L$a" cmp "\L$b" }
@{ $FHEM::Meta::maintainers{$maintainer}{packages} } )
{
if ($html) {
$pkgs .= '<br />' if ($counter);
$pkgs .=
'<a href="?cmd=get '
. $hash->{NAME}
. ' showPackageInfo '
. $pkg
. $FW_CSRF . '">'
. $pkg . '</a>';
}
else {
$pkgs .= "\n" unless ($counter);
$pkgs .= $pkg;
}
$counter++;
}
}
$l .= $tdOpen . $maintainer . $tdClose;
$l .= $tdOpen . $mods . $tdClose;
$l .= $tdOpen . $pkgs . $tdClose;
$l .= $trClose;
push @ret, $l;
$linecount++;
}
}
push @ret, $tClose . $blockClose if ($foundMaintainers);
# search for matching Perl package
my $foundPerl = 0;
$linecount = 1;
foreach my $dependent (
sort { "\L$a" cmp "\L$b" }
keys %{ $FHEM::Meta::dependents{pkgs} }
)
{
next if ( FHEM::Meta::ModuleIsPerlCore($dependent) );
next if ( FHEM::Meta::ModuleIsInternal($dependent) );
if ( $dependent =~ m/^.*$search.*$/i ) {
unless ($foundPerl) {
push @ret,
$blockOpen
. $tOpen
. $tCOpen
. ( $html ? '<a name="searchResultPerl"></a>' : '' )
. 'Perl Packages'
. $tCClose
. $tHOpen
. $trOpen;
push @ret, $thOpen . 'Name' . $thClose;
push @ret, $thOpen . 'Referenced from' . $thClose;
push @ret, $trClose . $tHClose;
}
$found++;
$foundPerl++;
my $l = $linecount % 2 == 0 ? $trOpenEven : $trOpenOdd;
my $references = '';
my $counter = 0;
foreach my $pkgReq (qw(requires recommends suggests)) {
next
unless (
defined(
$FHEM::Meta::dependents{pkgs}{$dependent}{$pkgReq}
)
);
foreach my $mod ( sort { "\L$a" cmp "\L$b" }
@{ $FHEM::Meta::dependents{pkgs}{$dependent}{$pkgReq} } )
{
if ($html) {
$references .= '<br />' if ($counter);
$references .=
'<a href="?cmd=get '
. $hash->{NAME}
. (
FHEM::Meta::ModuleIsInternal($mod) eq 'module'
? ' showModuleInfo '
: ' showPackageInfo '
)
. $mod
. $FW_CSRF . '">'
. $mod . '</a>';
}
else {
$references .= "\n" unless ($counter);
$references .= $mod;
}
$counter++;
}
}
$l .= $tdOpen . $dependent . $tdClose;
$l .= $tdOpen . $references . $tdClose;
$l .= $trClose;
push @ret, $l;
$linecount++;
}
}
push @ret, $tClose . $blockClose if ($foundPerl);
#TODO works only if fhem.pl patch was accepted:
# https://forum.fhem.de/index.php/topic,98937.0.html
if ( defined( $hash->{CL} )
&& defined( $hash->{CL}{'.iDefCmdMethod'} )
&& $hash->{CL}{'.iDefCmdMethod'} eq 'always' )
{
my $cmdO = $hash->{CL}{'.iDefCmdOrigin'};
if ( defined( $cmds{$cmdO} )
&& defined( $hash->{CL}{'.iDefCmdOverwrite'} )
&& $hash->{CL}{'.iDefCmdOverwrite'} )
{
my $cmd = $search;
$cmd =~ s/^$cmdO//;
$cmd = $cmdO . '!' . ( $cmd && $cmd ne '' ? ' ' . $cmd : '' );
unshift @ret,
$lb
. $lb
. 'Did you mean to <a href="?cmd='
. $cmd
. $FW_CSRF
. '">run command '
. $cmdO
. '</a> instead?';
}
delete $hash->{CL}{'.iDefCmdOrigin'};
delete $hash->{CL}{'.iDefCmdMethod'};
delete $hash->{CL}{'.iDefCmdOverwrite'};
}
if ($found) {
unshift @ret,
$lb
. $space
. $space
. ( $html ? '<a href="#searchResultPerl">' : '' )
. $foundPerl . ' '
. ( $foundPerl > 1 ? 'Perl packages' : 'Perl package' )
. ( $html ? '</a>' : '' )
if ($foundPerl);
unshift @ret,
$lb
. $space
. $space
. ( $html ? '<a href="#searchResultMaintainers">' : '' )
. $foundMaintainers . ' '
. ( $foundMaintainers > 1 ? 'authors' : 'author' )
. ( $html ? '</a>' : '' )
if ($foundMaintainers);
unshift @ret,
$lb
. $space
. $space
. ( $html ? '<a href="#searchResultKeywords">' : '' )
. $foundKeywords . ' '
. ( $foundKeywords > 1 ? 'keywords' : 'keyword' )
. ( $html ? '</a>' : '' )
if ($foundKeywords);
unshift @ret,
$lb
. $space
. $space
. ( $html ? '<a href="#searchResultPackages">' : '' )
. $foundPackages . ' '
. ( $foundPackages > 1 ? 'packages' : 'package' )
. ( $html ? '</a>' : '' )
if ($foundPackages);
unshift @ret,
$lb
. $space
. $space
. ( $html ? '<a href="#searchResultModules">' : '' )
. $foundModules . ' '
. ( $foundModules > 1 ? 'modules' : 'module' )
. ( $html ? '</a>' : '' )
if ($foundModules);
unshift @ret,
$lb
. $space
. $space
. ( $html ? '<a href="#searchResultDevices">' : '' )
. $foundDevices . ' '
. ( $foundDevices > 1 ? 'devices' : 'device' )
. ( $html ? '</a>' : '' )
if ($foundDevices);
unshift @ret,
$found . ' total search ' . ( $found > 1 ? 'results:' : 'result:' );
}
else {
unshift @ret,
$tOpen
. $trOpenOdd
. $tdOpen
. 'Nothing found'
. $tdClose
. $trClose
. $tClose
. $lb
. $lb;
}
push @ret, $tdClose . $trClose . $tClose . $blockClose;
unshift @ret,
$blockOpen
. $blockClose
. ( $html ? '<a name="searchResultTOP"></a>' : '' )
. $blockOpen
. $tTitleOpen
. 'Search Result'
. $tTitleClose
. $tOpen
. $trOpen
. $tdOpen
. $blockOpen
. $blockClose;
return $header . join( "\n", @ret ) . $footer;
}
#TODO
# - show master/slave dependencies
# - show parent/child dependencies
# - show other dependant/related modules
# - fill empty keywords
# - Get Community Support URL from MAINTAINERS.txt
sub CreateMetadataList ($$$) {
my ( $hash, $getCmd, $modName ) = @_;
$modName = 'Global' if ( uc($modName) eq 'FHEM' );
my $modType = lc($getCmd) eq 'showmoduleinfo' ? 'module' : 'package';
# disable automatic links to FHEM devices
delete $FW_webArgs{addLinks};
return 'Unknown module ' . $modName
if ( $modType eq 'module' && !defined( $modules{$modName} ) );
FHEM::Meta::Load($modName);
return 'Unknown package ' . $modName
if ( $modType eq 'package'
&& !defined( $packages{$modName} ) );
return 'No metadata found about module '
. $modName
if (
$modType eq 'module'
&& ( !defined( $modules{$modName}{META} )
|| scalar keys %{ $modules{$modName}{META} } == 0 )
);
return 'No metadata found about package '
. $modName
if (
$modType eq 'package'
&& ( !defined( $packages{$modName}{META} )
|| scalar keys %{ $packages{$modName}{META} } == 0 )
);
my $modMeta =
$modType eq 'module'
? $modules{$modName}{META}
: $packages{$modName}{META};
my @ret;
my $html =
defined( $hash->{CL} ) && $hash->{CL}{TYPE} eq "FHEMWEB" ? 1 : 0;
my $header = '';
my $footer = '';
if ($html) {
$header =
'<html><div class="detLink installerBack" style="float:right"><a href="?detail='
. $hash->{NAME}
. '">&larr; back to FHEM Installer</a></div>';
$footer = '</html>';
}
my $blockOpen = '';
my $tTitleOpen = '';
my $tTitleClose = '';
my $tOpen = '';
my $tCOpen = '';
my $tCClose = '';
my $tHOpen = '';
my $tHClose = '';
my $tBOpen = '';
my $tBClose = '';
my $tFOpen = '';
my $tFClose = '';
my $trOpen = '';
my $trOpenEven = '';
my $trOpenOdd = '';
my $thOpen = '';
my $thOpen2 = '';
my $thOpen3 = '';
my $tdOpen = '';
my $tdOpen2 = '';
my $tdOpen3 = '';
my $tdOpen4 = '';
my $strongOpen = '';
my $strongClose = '';
my $tdClose = "\t\t\t";
my $thClose = "\t\t\t";
my $trClose = '';
my $tClose = '';
my $blockClose = '';
my $colorRed = '';
my $colorGreen = '';
my $colorClose = '';
if ($html) {
$blockOpen = '<div class="makeTable wide internals">';
$tTitleOpen = '<span class="mkTitle">';
$tTitleClose = '</span>';
$tOpen = '<table class="block wide internals wrapcolumns">';
$tCOpen = '<caption style="text-align: left; font-size: larger;">';
$tCClose = '</caption>';
$tHOpen = '<thead>';
$tHClose = '</thead>';
$tBOpen = '<tbody>';
$tBClose = '</tbody>';
$tFOpen = '<tfoot style="font-size: smaller;">';
$tFClose = '</tfoot>';
$trOpen = '<tr class="column">';
$trOpenEven = '<tr class="column even">';
$trOpenOdd = '<tr class="column odd">';
$thOpen = '<th style="text-align: left; vertical-align: top;">';
$thOpen2 =
'<th style="text-align: left; vertical-align: top;" colspan="2">';
$thOpen3 =
'<th style="text-align: left; vertical-align: top;" colspan="3">';
$tdOpen = '<td style="vertical-align: top;">';
$tdOpen2 = '<td style="vertical-align: top;" colspan="2">';
$tdOpen3 = '<td style="vertical-align: top;" colspan="3">';
$tdOpen4 = '<td style="vertical-align: top;" colspan="4">';
$strongOpen = '<strong>';
$strongClose = '</strong>';
$tdClose = '</td>';
$thClose = '</th>';
$trClose = '</tr>';
$tClose = '</table>';
$blockClose = '</div>';
$colorRed = '<span style="color: red">';
$colorGreen = '<span style="color: green">';
$colorClose = '</span>';
}
my @mAttrs = qw(
name
abstract
keywords
version
release_date
release_status
author
copyright
privacy
homepage
wiki
command_reference
community_support
commercial_support
bugtracker
version_control
license
description
);
my $space = $html ? '&nbsp;' : ' ';
my $lb = $html ? '<br />' : "\n";
my $lang = lc(
AttrVal(
$hash->{NAME}, 'language',
AttrVal( 'global', 'language', 'EN' )
)
);
my $FW_CSRF = (
defined( $defs{ $hash->{CL}{SNAME} }{CSRFTOKEN} )
? '&fwcsrf=' . $defs{ $hash->{CL}{SNAME} }{CSRFTOKEN}
: ''
);
push @ret,
$blockOpen
. $tTitleOpen
. ucfirst($modType)
. ' Information'
. $tTitleClose
. $tOpen;
my $linecount = 1;
foreach my $mAttr (@mAttrs) {
next
if (
$mAttr eq 'release_status'
&& ( !defined( $modMeta->{release_status} )
|| $modMeta->{release_status} eq 'stable' )
);
next
if (
$mAttr eq 'abstract'
&& ( !defined( $modMeta->{abstract} )
|| $modMeta->{abstract} eq 'n/a'
|| $modMeta->{abstract} eq '' )
);
next
if (
$mAttr eq 'description'
&& ( !defined( $modMeta->{description} )
|| $modMeta->{description} eq 'n/a'
|| $modMeta->{description} eq '' )
);
next
if (
$mAttr eq 'bugtracker'
&& ( !defined( $modMeta->{resources} )
|| !defined( $modMeta->{resources}{bugtracker} ) )
);
next
if (
$mAttr eq 'homepage'
&& ( !defined( $modMeta->{resources} )
|| !defined( $modMeta->{resources}{homepage} ) )
);
next
if (
$mAttr eq 'copyright'
&& ( !defined( $modMeta->{resources} )
|| !defined( $modMeta->{resources}{x_copyright} ) )
);
next
if (
$mAttr eq 'privacy'
&& ( !defined( $modMeta->{resources} )
|| !defined( $modMeta->{resources}{x_privacy} ) )
);
next
if (
$mAttr eq 'wiki'
&& ( !defined( $modMeta->{resources} )
|| !defined( $modMeta->{resources}{x_wiki} ) )
);
next
if (
$mAttr eq 'community_support'
&& ( !defined( $modMeta->{resources} )
|| !defined( $modMeta->{resources}{x_support_community} ) )
);
next
if (
$mAttr eq 'commercial_support'
&& ( !defined( $modMeta->{resources} )
|| !defined( $modMeta->{resources}{x_support_commercial} ) )
);
next
if (
$mAttr eq 'keywords'
&& ( !defined( $modMeta->{keywords} )
|| !@{ $modMeta->{keywords} } )
);
next
if ( $mAttr eq 'version'
&& ( !defined( $modMeta->{version} ) ) );
next
if (
$mAttr eq 'version_control'
&& ( !defined( $modMeta->{resources} )
|| !defined( $modMeta->{resources}{repository} ) )
);
next
if (
$mAttr eq 'release_date'
&& ( !defined( $modMeta->{x_release_date} )
&& !defined( $modMeta->{x_vcs} ) )
);
next
if ( $mAttr eq 'command_reference'
&& $modType eq 'package' );
my $l = $linecount % 2 == 0 ? $trOpenEven : $trOpenOdd;
my $mAttrName = $mAttr;
$mAttrName =~ s/_/$space/g;
$mAttrName =~ s/([\w'&]+)/\u\L$1/g;
my $webname =
AttrVal( $hash->{CL}{SNAME}, 'webname', 'fhem' );
$l .= $thOpen . $mAttrName . $thClose;
# these attributes do not exist under that name in META.json
if ( !defined( $modMeta->{$mAttr} ) ) {
$l .= $tdOpen;
if ( $mAttr eq 'release_date' ) {
if ( defined( $modMeta->{x_release_date} ) ) {
$l .= $modMeta->{x_release_date};
}
elsif ( defined( $modMeta->{x_vcs} ) ) {
$l .= $modMeta->{x_vcs}[7];
}
}
elsif ( $mAttr eq 'copyright' ) {
my $copyName = '© ';
my $copyEmail =
defined( $modMeta->{resources}{x_copyright}{mailto} )
? $modMeta->{resources}{x_copyright}{mailto}
: '';
my $copyWeb;
my $copyNameContact;
$copyName .= $modMeta->{x_vcs}[8] . ' '
if ( defined( $modMeta->{x_vcs} ) );
$copyName .=
$modMeta->{resources}{x_copyright}{title};
if ( defined( $modMeta->{resources} )
&& defined( $modMeta->{resources}{x_copyright} )
&& defined( $modMeta->{resources}{x_copyright}{web} ) )
{
$copyWeb = $modMeta->{resources}{x_copyright}{web};
}
if ( $html && $copyWeb ) {
$copyNameContact =
'<a href="'
. $copyWeb
. '" target="_blank">'
. $copyName . '</a>';
}
elsif ( $html && $copyEmail ) {
$copyNameContact =
'<a href="mailto:'
. $copyEmail . '">'
. $copyName . '</a>';
}
$l .= $copyNameContact ? $copyNameContact : $copyName;
}
elsif ( $mAttr eq 'privacy' ) {
my $title =
defined( $modMeta->{resources}{x_privacy}{title} )
? $modMeta->{resources}{x_privacy}{title}
: $modMeta->{resources}{x_privacy}{web};
$l .=
'<a href="'
. $modMeta->{resources}{x_privacy}{web}
. '" target="_blank">'
. $title . '</a>';
}
elsif ($mAttr eq 'homepage'
&& defined( $modMeta->{resources} )
&& defined( $modMeta->{resources}{homepage} ) )
{
my $title =
defined( $modMeta->{resources}{x_homepage_title} )
? $modMeta->{resources}{x_homepage_title}
: (
$modMeta->{resources}{homepage} =~ m/^.+:\/\/([^\/]+).*/
? $1
: $modMeta->{resources}{homepage}
);
$l .=
'<a href="'
. $modMeta->{resources}{homepage}
. '" target="_blank">'
. $title . '</a>';
}
elsif ( $mAttr eq 'command_reference' ) {
if ( defined( $hash->{CL} )
&& defined( $hash->{CL}{TYPE} )
&& $hash->{CL}{TYPE} eq 'FHEMWEB' )
{
$l .=
'<a href="/'
. $webname
. '/docs/commandref.html#'
. ( $modName eq 'Global' ? 'global' : $modName )
. '" target="_blank">Offline version</a>';
}
if ( defined( $modMeta->{resources} )
&& defined( $modMeta->{resources}{x_commandref} )
&& defined( $modMeta->{resources}{x_commandref}{web} ) )
{
my $title =
defined( $modMeta->{resources}{x_commandref}{title} )
? $modMeta->{resources}{x_commandref}{title}
: 'Online version';
$l .=
( $webname ? ' | ' : '' )
. '<a href="'
. $modMeta->{resources}{x_commandref}{web}
. '" target="_blank">'
. $title . '</a>';
}
}
elsif ($mAttr eq 'wiki'
&& defined( $modMeta->{resources} )
&& defined( $modMeta->{resources}{x_wiki} )
&& defined( $modMeta->{resources}{x_wiki}{web} ) )
{
my $title =
defined( $modMeta->{resources}{x_wiki}{title} )
? $modMeta->{resources}{x_wiki}{title}
: (
$modMeta->{resources}{x_wiki}{web} =~
m/^(?:https?:\/\/)?wiki\.fhem\.de/i ? 'FHEM Wiki'
: ''
);
$title = 'FHEM Wiki: ' . $title
if ( $title ne ''
&& $title !~ m/^FHEM Wiki/i
&& $modMeta->{resources}{x_wiki}{web} =~
m/^(?:https?:\/\/)?wiki\.fhem\.de/i );
$l .=
'<a href="'
. $modMeta->{resources}{x_wiki}{web}
. '" target="_blank">'
. $title . '</a>';
}
elsif ($mAttr eq 'community_support'
&& defined( $modMeta->{resources} )
&& defined( $modMeta->{resources}{x_support_community} )
&& defined( $modMeta->{resources}{x_support_community}{web} ) )
{
my $board = $modMeta->{resources}{x_support_community};
$board =
$modMeta->{resources}{x_support_community}{subCommunity}
if (
defined(
$modMeta->{resources}{x_support_community}{subCommunity}
)
);
my $title =
defined( $board->{title} ) ? $board->{title}
: (
$board->{web} =~ m/^(?:https?:\/\/)?forum\.fhem\.de/i
? 'FHEM Forum'
: ''
);
$title = 'FHEM Forum: ' . $title
if ( $title ne ''
&& $title !~ m/^FHEM Forum/i
&& $board->{web} =~ m/^(?:https?:\/\/)?forum\.fhem\.de/i );
$l .= 'Limited - '
if ( defined( $modMeta->{x_support_status} )
&& $modMeta->{x_support_status} eq 'limited' );
$l .=
'<a href="'
. $board->{web}
. '" target="_blank"'
. (
defined( $board->{description} )
? ' title="'
. $board->{description}
. '"'
: (
defined(
$modMeta->{resources}{x_support_community}
{description}
)
? ' title="'
. $modMeta->{resources}{x_support_community}
{description} . '"'
: ''
)
)
. '>'
. $title . '</a>';
}
elsif ($mAttr eq 'commercial_support'
&& defined( $modMeta->{resources} )
&& defined( $modMeta->{resources}{x_support_commercial} )
&& defined( $modMeta->{resources}{x_support_commercial}{web} ) )
{
my $title =
defined( $modMeta->{resources}{x_support_commercial}{title} )
? $modMeta->{resources}{x_support_commercial}{title}
: $modMeta->{resources}{x_support_commercial}{web};
$l .= 'Limited - '
if ( $modMeta->{x_support_status} eq 'limited' );
$l .=
'<a href="'
. $modMeta->{resources}{x_support_commercial}{web}
. '" target="_blank">'
. $title . '</a>';
}
elsif ($mAttr eq 'bugtracker'
&& defined( $modMeta->{resources} )
&& defined( $modMeta->{resources}{bugtracker} )
&& defined( $modMeta->{resources}{bugtracker}{web} ) )
{
my $title =
defined( $modMeta->{resources}{bugtracker}{x_web_title} )
? $modMeta->{resources}{bugtracker}{x_web_title}
: (
$modMeta->{resources}{bugtracker}{web} =~
m/^(?:https?:\/\/)?forum\.fhem\.de/i ? 'FHEM Forum'
: (
$modMeta->{resources}{bugtracker}{web} =~
m/^(?:https?:\/\/)?github\.com\/fhem/i
? 'Github Issues: ' . $modMeta->{name}
: $modMeta->{resources}{bugtracker}{web}
)
);
# add prefix if user defined title
$title = 'FHEM Forum: ' . $title
if ( $title ne ''
&& $title !~ m/^FHEM Forum/i
&& $modMeta->{resources}{bugtracker}{web} =~
m/^(?:https?:\/\/)?forum\.fhem\.de/i );
$title = 'Github Issues: ' . $title
if ( $title ne ''
&& $title !~ m/^Github issues/i
&& $modMeta->{resources}{bugtracker}{web} =~
m/^(?:https?:\/\/)?github\.com\/fhem/i );
$l .=
'<a href="'
. $modMeta->{resources}{bugtracker}{web}
. '" target="_blank">'
. $title . '</a>';
}
elsif ($mAttr eq 'version_control'
&& defined( $modMeta->{resources} )
&& defined( $modMeta->{resources}{repository} )
&& defined( $modMeta->{resources}{repository}{type} )
&& defined( $modMeta->{resources}{repository}{url} ) )
{
# Web link
if ( defined( $modMeta->{resources}{repository}{web} ) ) {
# master link
my $url =
$modMeta->{resources}{repository}{web};
if (
defined( $modMeta->{resources}{repository}{x_branch} )
&& defined( $modMeta->{resources}{repository}{x_dev} )
&& defined(
$modMeta->{resources}{repository}{x_dev}{x_branch}
)
)
{
my $bName =
$modMeta->{resources}{repository}{x_branch};
$bName = 'production'
if (
$modMeta->{resources}{repository}{x_branch} eq
$modMeta->{resources}{repository}{x_dev}{x_branch}
);
# webview: master entry
$l .=
'View online source code: <a href="'
. $url
. '" target="_blank">'
. $bName . '</a>';
# webview: dev link
$bName =
$modMeta->{resources}{repository}{x_dev}{x_branch};
$bName = 'development'
if (
$modMeta->{resources}{repository}{x_branch} eq
$modMeta->{resources}{repository}{x_dev}{x_branch}
);
$url =
$modMeta->{resources}{repository}{x_dev}{web};
# webview: dev entry
$l .=
' | <a href="'
. $url
. '" target="_blank">'
. $bName . '</a>';
# raw: master entry
if (
defined( $modMeta->{resources}{repository}{x_raw} )
)
{
$bName =
$modMeta->{resources}{repository}{x_branch};
$bName = 'production'
if (
$modMeta->{resources}{repository}{x_branch} eq
$modMeta->{resources}{repository}{x_dev}
{x_branch} );
$url = $modMeta->{resources}{repository}{x_raw};
$l .=
$lb
. 'Download raw file: <a href="'
. $url
. '" target="_blank" download="'
. $modMeta->{x_file}[2] . '">'
. $bName . '</a>';
# raw: dev link
if (
defined(
$modMeta->{resources}{repository}{x_dev}
{x_raw}
)
)
{
$bName =
$modMeta->{resources}{repository}{x_dev}
{x_branch};
$bName = 'development'
if (
$modMeta->{resources}{repository}{x_branch}
eq
$modMeta->{resources}{repository}{x_dev}
{x_branch} );
$url =
$modMeta->{resources}{repository}{x_dev}
{x_raw};
# raw: dev entry
$l .=
' | <a href="'
. $url
. '" target="_blank" download="'
. $modMeta->{x_file}[2] . '">'
. $bName . '</a>';
}
}
}
# master entry
else {
$l .=
'<a href="'
. $url
. '" target="_blank">View online source code</a>';
if (
defined( $modMeta->{resources}{repository}{x_raw} )
)
{
$l .=
$lb
. '<a href="'
. $modMeta->{resources}{repository}{x_raw}
. '" target="_blank" download="'
. $modMeta->{x_file}[2]
. '">Download raw file</a>';
}
}
$l .= $lb;
}
# VCS link
my $urlPrefix = (
$modMeta->{resources}{repository}{url} =~
/^$modMeta->{resources}{repository}{type}/i
? ''
: lc( $modMeta->{resources}{repository}{type} ) . '+'
);
$l .=
uc( $modMeta->{resources}{repository}{type} )
. ' repository: '
. '<a href="'
. $urlPrefix
. $modMeta->{resources}{repository}{url}
. '" target="_blank">'
. $urlPrefix
. $modMeta->{resources}{repository}{url} . '</a>';
if ( defined( $modMeta->{resources}{repository}{x_branch} ) ) {
if (
lc( $modMeta->{resources}{repository}{type} ) eq 'svn' )
{
$l .=
$lb
. 'Main branch: '
. '<a href="'
. $urlPrefix
. $modMeta->{resources}{repository}{url} . '/'
. (
$modMeta->{resources}{repository}{x_branch} eq
'trunk'
? 'trunk'
: 'branches/'
. $modMeta->{resources}{repository}{x_branch}
)
. '" target="_blank">'
. $modMeta->{resources}{repository}{x_branch}
. '</a>';
}
else {
$l .=
$lb
. 'Main branch: '
. $modMeta->{resources}{repository}{x_branch};
}
}
if (
defined( $modMeta->{resources}{repository}{x_branch} )
&& defined( $modMeta->{resources}{repository}{x_dev} )
&& defined(
$modMeta->{resources}{repository}{x_dev}{x_branch}
)
&& $modMeta->{resources}{repository}{x_branch} ne
$modMeta->{resources}{repository}{x_dev}{x_branch}
)
{
if (
lc( $modMeta->{resources}{repository}{x_dev}{type} ) eq
'svn' )
{
$l .=
$lb
. 'Dev branch: '
. '<a href="'
. $urlPrefix
. $modMeta->{resources}{repository}{url} . '/'
. (
$modMeta->{resources}{repository}{x_branch} eq
'trunk'
? 'trunk'
: 'branches/'
. $modMeta->{resources}{repository}{x_branch}
)
. '" target="_blank">'
. $modMeta->{resources}{repository}{x_dev}{x_branch}
. '</a>';
}
else {
$l .=
$lb
. 'Dev branch: '
. $modMeta->{resources}{repository}{x_dev}{x_branch};
}
}
}
else {
$l .= '-';
}
$l .= $tdClose;
}
# these text attributes can be shown directly
elsif ( !ref( $modMeta->{$mAttr} ) ) {
$l .= $tdOpen;
my $mAttrVal =
defined( $modMeta->{x_lang} )
&& defined( $modMeta->{x_lang}{$lang} )
&& defined( $modMeta->{x_lang}{$lang}{$mAttr} )
? $modMeta->{x_lang}{$lang}{$mAttr}
: $modMeta->{$mAttr};
$mAttrVal =~ s/\\n/$lb/g;
if ( $mAttr eq 'version' ) {
if ( $mAttrVal eq '0.000000001' ) {
$mAttrVal = '-';
}
elsif ( $modMeta->{x_file}[7] ne 'generated/vcs' ) {
$mAttrVal = version->parse($mAttrVal)->normal;
# only show maximum featurelevel for fhem.pl
$mAttrVal = $1
if ( $modName eq 'Global'
&& $mAttrVal =~ m/^(v\d+\.\d+).*/ );
# Only add commit revision when it is not
# part of the version already
$mAttrVal .= '-s' . $modMeta->{x_vcs}[5]
if ( defined( $modMeta->{x_vcs} )
&& $modMeta->{x_vcs}[5] ne '' );
}
}
# Add filename to module name
$mAttrVal .= ' (' . $modMeta->{x_file}[2] . ')'
if ( $modType eq 'module'
&& $mAttr eq 'name'
&& $modName ne 'Global' );
$l .= $mAttrVal . $tdClose;
}
# this attribute is an array and needs further processing
elsif (ref( $modMeta->{$mAttr} ) eq 'ARRAY'
&& @{ $modMeta->{$mAttr} } > 0
&& $modMeta->{$mAttr}[0] ne '' )
{
$l .= $tdOpen;
if ( $mAttr eq 'license' ) {
if ( defined( $modMeta->{resources} )
&& defined( $modMeta->{resources}{license} )
&& ref( $modMeta->{resources}{license} ) eq 'ARRAY'
&& @{ $modMeta->{resources}{license} } > 0
&& $modMeta->{resources}{license}[0] ne '' )
{
$l .=
'<a href="'
. $modMeta->{resources}{license}[0]
. '" target="_blank">'
. $modMeta->{$mAttr}[0] . '</a>';
}
else {
$l .= $modMeta->{$mAttr}[0];
}
}
elsif ( $mAttr eq 'author' ) {
my $authorCount = scalar @{ $modMeta->{$mAttr} };
my $counter = 0;
foreach ( @{ $modMeta->{$mAttr} } ) {
next if ( $_ eq '' );
my $authorName;
my $authorEditorOnly;
my $authorEmail;
if ( $_ =~
m/^([^<>\n\r]+?)(?:\s+(\(last release only\)))?(?:\s+(?:<(.*)>))?$/
)
{
$authorName = $1;
$authorEditorOnly = $2 ? ' ' . $2 : '';
$authorEmail = $3;
}
my $authorNameEmail = $authorName;
# add alias name if different
if ( defined( $modMeta->{x_fhem_maintainer} )
&& ref( $modMeta->{x_fhem_maintainer} ) eq 'ARRAY'
&& @{ $modMeta->{x_fhem_maintainer} } > 0
&& $modMeta->{x_fhem_maintainer}[$counter] ne '' )
{
my $alias = $modMeta->{x_fhem_maintainer}[$counter];
if ( $alias eq $authorName ) {
$authorNameEmail =
'<a href="?cmd=get '
. $hash->{NAME}
. ' search '
. $alias
. $FW_CSRF . '">'
. $authorName . '</a>'
. $authorEditorOnly
if ($html);
}
else {
if ($html) {
$authorNameEmail =
$authorName
. ', alias <a href="?cmd=get '
. $hash->{NAME}
. ' search '
. $alias
. $FW_CSRF . '">'
. $alias . '</a>'
. $authorEditorOnly;
}
else {
$authorNameEmail =
$authorName
. $authorEditorOnly
. ', alias '
. $alias;
}
}
}
$l .= $lb if ($counter);
$l .= $lb . 'Co-' . $mAttrName . ':' . $lb
if ( $counter == 1 );
$l .=
$authorNameEmail
? $authorNameEmail
: $authorName . $authorEditorOnly;
$counter++;
}
}
elsif ( $mAttr eq 'keywords' ) {
my $counter = 0;
foreach my $keyword ( @{ $modMeta->{$mAttr} } ) {
$l .= ', ' if ($counter);
my $descr = FHEM::Meta::GetKeywordDesc( $keyword, $lang );
if ($html) {
$l .=
'<a href="?cmd=get '
. $hash->{NAME}
. ' search '
. $keyword
. $FW_CSRF . '"'
. (
$descr ne ''
? ' title="' . $descr . '"'
: ''
)
. '>'
. $keyword . '</a>';
}
else {
$l .= $keyword;
}
$counter++;
}
}
else {
$l .= join ', ', @{ $modMeta->{$mAttr} };
}
$l .= $tdClose;
}
# woops, we don't know how to handle this attribute
else {
$l .= $tdOpen . '?' . $tdClose;
}
$l .= $trClose;
push @ret, $l;
$linecount++;
}
push @ret,
$tFOpen
. $trOpen
. (
$html
? '<td style="text-align:right;" colspan="2">'
: ''
)
. 'Based on data generated by '
. $lb
. $modMeta->{generated_by}
. $tdClose
. $trClose
. $tFClose;
push @ret, $tClose . $blockClose;
# show FHEM modules who use this package
@mAttrs = qw(
requires
recommends
suggests
);
$linecount = 1;
foreach my $mAttr (@mAttrs) {
next
unless ( defined( $FHEM::Meta::dependents{pkgs}{$modName}{$mAttr} )
&& ref( $FHEM::Meta::dependents{pkgs}{$modName}{$mAttr} ) eq
'ARRAY'
&& @{ $FHEM::Meta::dependents{pkgs}{$modName}{$mAttr} } > 0 );
my $dependents = '';
my $counter = 0;
foreach my $dependant ( sort { "\L$a" cmp "\L$b" }
@{ $FHEM::Meta::dependents{pkgs}{$modName}{$mAttr} } )
{
my $link = $dependant;
$link =
'<a href="?cmd=get '
. $hash->{NAME}
. (
FHEM::Meta::ModuleIsInternal($dependant) eq 'module'
? ' showModuleInfo '
: ' showPackageInfo '
)
. $dependant
. $FW_CSRF . '">'
. $dependant . '</a>'
if ($html);
$dependents .= ', ' if ($counter);
$dependents .= $link;
$counter++;
}
if ( $dependents ne '' ) {
if ( $linecount == 1 ) {
push @ret,
$blockOpen
. $tTitleOpen
. 'FHEM internal dependencies'
. $tTitleClose
. $tOpen;
push @ret, $tHOpen . $trOpen;
push @ret, $thOpen . 'Importance' . $thClose;
push @ret, $thOpen . 'Dependent Modules' . $thClose;
push @ret, $trClose . $tHClose;
}
my $l = $linecount % 2 == 0 ? $trOpenEven : $trOpenOdd;
my $importance = $mAttr;
$importance = 'required' if ( $mAttr eq 'requires' );
$importance = 'recommended' if ( $mAttr eq 'recommends' );
$importance = 'suggested' if ( $mAttr eq 'suggests' );
$l .= $tdOpen . $importance . $tdClose;
$l .= $tdOpen . $dependents . $tdClose;
$l .= $trClose;
push @ret, $l;
$linecount++;
}
}
push @ret,
$tFOpen
. $trOpen
. $tdOpen2
. $strongOpen . 'Hint:'
. $strongClose
. ' Dependents can only be shown here if they were loaded into the metadata cache before.'
. $tdClose
. $trClose
. $tFClose
. $tClose
. $blockClose
if ( $linecount > 1 );
if (
$modType eq 'module'
&& $modName ne 'Global'
&& ( !defined( $modules{$modName}{META} )
|| !defined( $modules{$modName}{META}{keywords} )
|| !
grep ( /^fhem-mod-command$/,
@{ $modules{$modName}{META}{keywords} } ) )
)
{
push @ret, $blockOpen . $tTitleOpen . 'Devices' . $tTitleClose . $tOpen;
my $linecount = 1;
if ( defined( $modules{$modName}{LOADED} )
&& $modules{$modName}{LOADED} )
{
my @instances = devspec2array( 'TYPE=' . $modName );
if ( @instances > 0 ) {
push @ret,
$tHOpen
. $trOpen
. $thOpen . 'Name'
. $thClose
. $thOpen . 'State'
. $thClose
. $trClose
. $tHClose
. $tBOpen;
foreach my $instance ( sort { "\L$a" cmp "\L$b" } @instances ) {
next if ( defined( $defs{$instance}{TEMPORARY} ) );
my $l = $linecount % 2 == 0 ? $trOpenEven : $trOpenOdd;
my $device = $instance;
$device =
'<a href="?detail='
. $instance . '">'
. $instance . '</a>'
if ($html);
$l .= $tdOpen . $device . $tdClose;
$l .= $tdOpen . $defs{$instance}{STATE} . $tdClose;
push @ret, $l;
$linecount++;
}
push @ret, $tBClose;
}
else {
push @ret,
$tBOpen
. $trOpen
. $tdOpen
. 'The module was once loaded into memory, '
. 'but currently there is no device defined.'
. $tdClose
. $trClose
. $tBClose;
}
}
else {
push @ret,
$tBOpen
. $trOpen
. $tdOpen
. 'The module is currently not in use.'
. $tdClose
. $trClose
. $tBClose;
}
push @ret, $tClose . $blockClose;
}
LoadInstallStatusPerl($modName);
push @ret,
$blockOpen
. $tTitleOpen
. 'System Prerequisites'
. $tTitleClose
. $tOpen
. $trOpen
. $tdOpen;
push @ret, $blockOpen . $tOpen . $tCOpen . 'Perl Packages' . $tCClose;
if ( defined( $modMeta->{prereqs} )
&& defined( $modMeta->{prereqs}{runtime} ) )
{
@mAttrs = qw(
requires
recommends
suggests
);
push @ret, $tHOpen . $trOpen;
push @ret, $thOpen . 'Name' . $thClose;
push @ret, $thOpen . 'Importance' . $thClose;
push @ret, $thOpen . 'Status' . $thClose;
push @ret, $thOpen . 'Action' . $thClose if ($html);
push @ret, $trClose . $tHClose . $tBOpen;
$linecount = 1;
foreach my $mAttr (@mAttrs) {
next
unless ( defined( $modMeta->{prereqs}{runtime}{$mAttr} )
&& keys %{ $modMeta->{prereqs}{runtime}{$mAttr} } > 0 );
foreach
my $prereq ( sort keys %{ $modMeta->{prereqs}{runtime}{$mAttr} } )
{
my $isFhem = FHEM::Meta::ModuleIsInternal($prereq);
my $installed = $pkgStatus{Perl}{pkgs}{$prereq}{status};
my $l = $linecount % 2 == 0 ? $trOpenEven : $trOpenOdd;
my $importance = $mAttr;
$importance = 'required' if ( $mAttr eq 'requires' );
$importance = 'recommended' if ( $mAttr eq 'recommends' );
$importance = 'suggested' if ( $mAttr eq 'suggests' );
my $version = $modMeta->{prereqs}{runtime}{$mAttr}{$prereq};
$version = '' if ( !defined($version) || $version eq '0' );
my $action = '';
my $inherited = '';
if (
defined( $modMeta->{prereqs}{runtime}{x_inherited} )
&& defined(
$modMeta->{prereqs}{runtime}{x_inherited}{$prereq}
)
)
{
$inherited = '[inherited]';
$inherited = '<span title="Inherited from '
. join(
', ',
@{
$modMeta->{prereqs}{runtime}{x_inherited}{$prereq}
}
)
. '" style="color: grey;">'
. $inherited
. '</span>'
if ($html);
}
if (
$mAttr ne 'requires'
&& ( $installed eq 'missing'
|| $installed eq 'outdated' )
)
{
$installed = '';
$action =
'<div class="detLink installerAction"><a href="?cmd=set '
. $hash->{NAME}
. ' installPerl '
. $prereq . ' '
. $FW_CSRF
. '" title="Click here to trigger installation">'
. ( $installed eq 'outdated' ? 'Update' : 'Install' )
. '</a></div>';
}
elsif ( $installed eq 'installed' ) {
$installed = $colorGreen . $installed . $colorClose;
}
elsif ($installed eq 'missing'
|| $installed eq 'outdated' )
{
$installed =
$colorRed
. $strongOpen
. uc($installed)
. $strongClose
. $colorClose;
$action =
'<div class="detLink installerAction"><a href="?cmd=set '
. $hash->{NAME}
. ' installPerl '
. $prereq . ' '
. $FW_CSRF
. '" title="Click here to trigger installation">'
. ( $installed eq 'outdated' ? 'Update' : 'Install' )
. '</a></div>';
}
$prereq =
'<a href="https://metacpan.org/pod/'
. $prereq
. '" target="_blank">'
. $prereq . '</a>'
if ( $html
&& $installed ne 'built-in'
&& $installed ne 'included' );
$prereq =
'<a href="?cmd=get '
. $hash->{NAME}
. (
$isFhem eq 'module'
? ' showModuleInfo '
: ' showPackageInfo '
)
. $prereq
. $FW_CSRF . '">'
. $prereq . '</a>'
if ( $html
&& $installed eq 'included' );
$l .=
$tdOpen
. $prereq
. ( $inherited ne '' ? " $inherited" : '' )
. ( $version ne '' ? " ($version)" : '' )
. $tdClose;
$l .= $tdOpen . $importance . $tdClose;
$l .= $tdOpen . $installed . $tdClose;
$l .= $tdOpen . $action . $tdClose if ($html);
$l .= $trClose;
push @ret, $l;
$linecount++;
}
}
push @ret, $tBClose;
push @ret,
$tFOpen
. $trOpenEven
. ( $html ? $tdOpen4 : $tdOpen3 )
. $strongOpen . 'Hint:'
. $strongClose
. ' The module does not provide Perl prerequisites from its metadata.'
. $lb
. 'This result is based on automatic source code analysis '
. 'and can be incorrect. '
. 'Suggested Perl items may still be required if the module author had decided to implement some own dependency and/or error handling like returning an informative message instead of the original Perl error message.'
. $tdClose
. $trClose
. $tFClose
if ( defined( $modMeta->{x_prereqs_src} )
&& $modMeta->{x_prereqs_src} ne 'META.json' );
}
elsif ( defined( $modMeta->{x_prereqs_src} ) ) {
push @ret,
$tBOpen
. $trOpenOdd
. $tdOpen
. 'No known prerequisites.'
. $tdClose
. $trClose
. $tBClose;
}
else {
push @ret,
$tBOpen
. $trOpenOdd
. $tdOpen
. 'Module metadata do not contain any prerequisites.' . "\n"
. (
$html
? '<a href="?cmd=set '
. $hash->{NAME}
. ' installPerl Perl::PrereqScanner::NotQuiteLite'
. $FW_CSRF
. '">Click here to install Perl::PrereqScanner::NotQuiteLite</a> for automatic source code analysis.'
: 'For automatic source code analysis, please install Perl::PrereqScanner::NotQuiteLite first.'
)
. $tdClose
. $trClose
. $tBClose;
}
push @ret, $tClose . $blockClose;
if ( defined( $modMeta->{x_prereqs_nodejs} )
&& defined( $modMeta->{x_prereqs_nodejs}{runtime} ) )
{
push @ret,
$blockOpen
. $tTitleClose
. $tOpen
. $tCOpen
. 'Node.js Packages'
. $tCClose;
my @mAttrs = qw(
requires
recommends
suggests
);
push @ret, $tHOpen . $trOpen;
push @ret, $thOpen . 'Name' . $thClose;
push @ret, $thOpen . 'Importance' . $thClose;
push @ret, $thOpen . 'Status' . $thClose;
push @ret, $trClose . $tHClose . $tBOpen;
$linecount = 1;
foreach my $mAttr (@mAttrs) {
next
unless ( defined( $modMeta->{x_prereqs_nodejs}{runtime}{$mAttr} )
&& keys %{ $modMeta->{x_prereqs_nodejs}{runtime}{$mAttr} } >
0 );
foreach my $prereq (
sort
keys %{ $modMeta->{x_prereqs_nodejs}{runtime}{$mAttr} }
)
{
my $l = $linecount % 2 == 0 ? $trOpenEven : $trOpenOdd;
my $importance = $mAttr;
$importance = 'required' if ( $mAttr eq 'requires' );
$importance = 'recommended' if ( $mAttr eq 'recommends' );
$importance = 'suggested' if ( $mAttr eq 'suggests' );
my $version =
$modMeta->{x_prereqs_nodejs}{runtime}{$mAttr}{$prereq};
$version = '' if ( !defined($version) || $version eq '0' );
my $check = __IsInstalledNodejs($prereq);
my $installed = '';
if ($check) {
if ( $check =~ m/^\d+\./ ) {
my $nverReq =
$version ne ''
? $version
: 0;
my $nverInst = $check;
#TODO suport for version range:
#https://metacpan.org/pod/CPAN::Meta::Spec#Version-Range
if ( $nverReq > 0 && $nverInst < $nverReq ) {
$installed .=
$colorRed
. 'OUTDATED'
. $colorClose . ' ('
. $check . ')';
}
else {
$installed = 'installed';
}
}
else {
$installed = 'installed';
}
}
else {
$installed = $colorRed . 'MISSING' . $colorClose
if ( $importance eq 'required' );
}
$installed = $colorGreen . $installed . $colorClose;
$prereq =
'<a href="https://www.npmjs.com/package/'
. $prereq
. '" target="_blank">'
. $prereq . '</a>'
if ($html);
$l .=
$tdOpen
. $prereq
. ( $version ne '' ? " ($version)" : '' )
. $tdClose;
$l .= $tdOpen . $importance . $tdClose;
$l .= $tdOpen . $installed . $tdClose;
$l .= $trClose;
push @ret, $l;
$linecount++;
}
}
push @ret, $tBClose . $tClose . $blockClose;
}
if ( defined( $modMeta->{x_prereqs_python} )
&& defined( $modMeta->{x_prereqs_python}{runtime} ) )
{
push @ret, $blockOpen . $tOpen . $tCOpen . 'Python Packages' . $tCClose;
my @mAttrs = qw(
requires
recommends
suggests
);
push @ret, $tHOpen . $trOpen;
push @ret, $thOpen . 'Name' . $thClose;
push @ret, $thOpen . 'Importance' . $thClose;
push @ret, $thOpen . 'Status' . $thClose;
push @ret, $trClose . $tHClose . $tBOpen;
$linecount = 1;
foreach my $mAttr (@mAttrs) {
next
unless ( defined( $modMeta->{x_prereqs_python}{runtime}{$mAttr} )
&& keys %{ $modMeta->{x_prereqs_python}{runtime}{$mAttr} } >
0 );
foreach my $prereq (
sort
keys %{ $modMeta->{x_prereqs_python}{runtime}{$mAttr} }
)
{
my $l = $linecount % 2 == 0 ? $trOpenEven : $trOpenOdd;
my $importance = $mAttr;
$importance = 'required' if ( $mAttr eq 'requires' );
$importance = 'recommended' if ( $mAttr eq 'recommends' );
$importance = 'suggested' if ( $mAttr eq 'suggests' );
my $version =
$modMeta->{x_prereqs_python}{runtime}{$mAttr}{$prereq};
$version = '' if ( !defined($version) || $version eq '0' );
my $check = __IsInstalledPython($prereq);
my $installed = '';
if ($check) {
if ( $check =~ m/^\d+\./ ) {
my $nverReq =
$version ne ''
? $version
: 0;
my $nverInst = $check;
#TODO suport for version range:
#https://metacpan.org/pod/CPAN::Meta::Spec#Version-Range
if ( $nverReq > 0 && $nverInst < $nverReq ) {
$installed .=
$colorRed
. 'OUTDATED'
. $colorClose . ' ('
. $check . ')';
}
else {
$installed = 'installed';
}
}
else {
$installed = 'installed';
}
}
else {
$installed = $colorRed . 'MISSING' . $colorClose
if ( $importance eq 'required' );
}
my $isPerlCore = FHEM::Meta::ModuleIsPerlCore($prereq);
my $isFhem =
$isPerlCore
? 0
: FHEM::Meta::ModuleIsInternal($prereq);
if ( $isPerlCore || $prereq eq 'perl' ) {
$installed =
$installed ne 'installed'
? "$installed (Perl built-in)"
: 'built-in';
}
elsif ($isFhem) {
$installed =
$installed ne 'installed'
? "$installed (FHEM included)"
: 'included';
}
elsif ( $installed eq 'installed' ) {
$installed = $colorGreen . $installed . $colorClose;
}
$prereq =
'<a href="https://metacpan.org/pod/'
. $prereq
. '" target="_blank">'
. $prereq . '</a>'
if ( $html
&& !$isFhem
&& !$isPerlCore
&& $prereq ne 'perl' );
$l .=
$tdOpen
. $prereq
. ( $version ne '' ? " ($version)" : '' )
. $tdClose;
$l .= $tdOpen . $importance . $tdClose;
$l .= $tdOpen . $installed . $tdClose;
$l .= $trClose;
push @ret, $l;
$linecount++;
}
}
push @ret, $tBClose . $tClose . $blockClose;
}
push @ret, $tdClose . $trClose . $tClose . $blockClose;
return $header . join( "\n", @ret ) . $footer;
}
sub CreateRawMetaJson ($$$) {
my ( $hash, $getCmd, $modName ) = @_;
$modName = 'Global' if ( uc($modName) eq 'FHEM' );
my $modType = lc($getCmd) eq 'zzgetmodulemeta.json' ? 'module' : 'package';
FHEM::Meta::Load($modName);
return '{}'
unless (
(
$modType eq 'module'
&& defined( $modules{$modName}{META} )
&& scalar keys %{ $modules{$modName}{META} } > 0
)
|| ( $modType eq 'package'
&& defined( $packages{$modName}{META} )
&& scalar keys %{ $packages{$modName}{META} } > 0 )
);
my $j = JSON->new;
$j->allow_nonref;
$j->canonical;
$j->pretty;
if ( $modType eq 'module' ) {
return $j->encode( $modules{$modName}{META} );
}
else {
return $j->encode( $packages{$modName}{META} );
}
}
sub __GetDefinedModulesFromFile($) {
my ($filePath) = @_;
my @modules;
my $fh;
if ( open( $fh, '<' . $filePath ) ) {
while ( my $l = <$fh> ) {
if ( $l =~ /^define\s+\S+\s+(\S+).*/ ) {
my $modName = $1;
push @modules, $modName
unless ( grep ( /^$modName$/, @modules ) );
}
}
close($fh);
}
if (wantarray) {
return @modules;
}
elsif ( @modules > 0 ) {
return join( ',', @modules );
}
}
sub LoadInstallStatusPerl(;$) {
my ($modList) = @_;
my $t = TimeNow();
my @rets;
my $unused = 0;
my @lmodules;
# if modList is undefined or is equal to '1'
if ( !$modList || ( !ref($modList) && $modList eq '1' ) ) {
$unused = 1 if ( $modList && $modList eq '1' );
foreach ( keys %modules ) {
# Only process loaded modules
# unless unused modules were
# explicitly requested
push @lmodules,
$_
if (
$unused
|| ( defined( $modules{$_}{LOADED} )
&& $modules{$_}{LOADED} eq '1' )
);
}
}
# if a single module name was given
elsif ( !ref($modList) ) {
push @lmodules, $modList;
}
# if a list of module names was given
elsif ( ref($modList) eq 'ARRAY' ) {
foreach ( @{$modList} ) {
push @lmodules, $_;
}
}
# if a hash was given, assume every
# key is a module name
elsif ( ref($modList) eq 'HASH' ) {
foreach ( keys %{$modList} ) {
push @lmodules, $_;
}
}
# Wrong method use
else {
$@ =
__PACKAGE__ . "LoadInstallStatusPerl: ERROR: Unknown parameter value";
Log 1, $@;
return "$@";
}
foreach my $modName (@lmodules) {
$modName = 'Global' if ( uc($modName) eq 'FHEM' );
my $type;
if ( exists( $modules{$modName} )
&& !exists( $packages{$modName} ) )
{
$type = 'module';
}
elsif ( exists( $packages{$modName} )
&& !exists( $modules{$modName} ) )
{
$type = 'package';
}
elsif (exists( $packages{$modName} )
&& exists( $modules{$modName} ) )
{
$type = 'module+package';
}
next unless ($type);
foreach my $type ( split( '\+', $type ) ) {
FHEM::Meta::Load($modName);
next
unless (
( $type eq 'module' && defined( $modules{$modName}{META} ) )
|| ( $type eq 'package'
&& defined( $packages{$modName}{META} ) )
);
my $modMeta =
$type eq 'module'
? $modules{$modName}{META}
: $packages{$modName}{META};
$pkgStatus{Perl}{analyzed} = 2
unless ( defined( $modMeta->{x_prereqs_src} ) );
# Perl
if ( defined( $modMeta->{prereqs} )
&& defined( $modMeta->{prereqs}{runtime} ) )
{
my $modPreqs = $modMeta->{prereqs}{runtime};
foreach my $mAttr (qw(requires recommends suggests)) {
next
unless ( defined( $modPreqs->{$mAttr} )
&& keys %{ $modPreqs->{$mAttr} } > 0 );
foreach my $pkg ( keys %{ $modPreqs->{$mAttr} } ) {
push
@{ $pkgStatus{Perl}{pkgs}{$pkg}{ $type . 's' }{$mAttr}
},
$modName
unless (
grep ( /^$modName$/,
@{
$pkgStatus{Perl}{pkgs}{$pkg}
{ $type . 's' }{$mAttr}
} )
);
next
if (
defined( $pkgStatus{Perl}{pkgs}{$pkg}{status} ) );
my $fname = $pkg;
$fname =~
s/^.*://g; # strip away any parent module names
my $isPerlCore = FHEM::Meta::ModuleIsPerlCore($pkg);
my $isFhem =
$isPerlCore
? 0
: FHEM::Meta::ModuleIsInternal($pkg);
if ( $pkg eq 'perl' ) {
$pkgStatus{Perl}{pkgs}{$pkg}{status} =
'built-in';
$pkgStatus{Perl}{installed}{$pkg} =
version->parse($])->numify;
}
elsif ( $pkg eq 'FHEM' ) {
$pkgStatus{Perl}{pkgs}{$pkg}{status} =
'included';
$pkgStatus{Perl}{installed}{$pkg} =
$modules{'Global'}{META}{version};
}
elsif ( $pkg eq 'FHEM::Meta' || $pkg eq 'Meta' ) {
$pkgStatus{Perl}{pkgs}{$pkg}{status} =
'included';
$pkgStatus{Perl}{installed}{$pkg} =
FHEM::Meta->VERSION();
}
elsif ($isPerlCore) {
$pkgStatus{Perl}{pkgs}{$pkg}{status} =
'built-in';
$pkgStatus{Perl}{installed}{$pkg} = 0;
}
# This is a FHEM package
elsif ( $isFhem && $isFhem eq 'package' ) {
$pkgStatus{Perl}{pkgs}{$pkg}{status} =
'included';
$pkgStatus{Perl}{installed}{$pkg} =
defined( $packages{$fname}{META} )
? $packages{$fname}{META}{version}
: 0;
}
# This is a FHEM module being loaded as package
elsif ( $isFhem && $isFhem eq 'module' ) {
$pkgStatus{Perl}{pkgs}{$pkg}{status} =
'included';
$pkgStatus{Perl}{installed}{$pkg} =
defined( $modules{$fname}{META} )
? $modules{$fname}{META}{version}
: 0;
}
elsif ( $pkg =~ /^Win32::/ && $^O !~ /Win/ ) {
$pkgStatus{Perl}{pkgs}{$pkg}{status} = 'n/a';
}
else {
my $pkgpath = $pkg . '.pm';
$pkgpath =~ s/::/\//g;
# remove any ealier tries to load
# to get the original error message
foreach ( keys %INC ) {
delete $INC{$_}
if ( !$INC{$_} );
}
#FIXME disable warnings does not work here...
no warnings;
my $verbose = AttrVal( 'global', 'verbose', 3 );
$attr{global}{verbose} = 0;
eval "no warnings; require $pkg;";
$attr{global}{verbose} = $verbose;
use warnings;
if ( $@ && $@ =~ m/^Can't locate (\S+)\.pm/i ) {
my $missing = $1;
$missing =~ s/\//::/g;
$pkgStatus{Perl}{pkgs}{$missing}{status} =
'missing';
push @{ $pkgStatus{Perl}{missing}{$missing} },
defined( $modPreqs->{$mAttr}{$missing} )
? $modPreqs->{$mAttr}{$missing}
: 0;
$pkgStatus{Perl}{analyzed} = 1
if ( $modMeta->{x_prereqs_src} ne 'META.json'
&& !$pkgStatus{Perl}{analyzed} );
# If the error message does contain a
# different package name,
# the actual package is installed and
# misses another package by it's own
if ( $missing ne $pkg ) {
my $v = eval "$pkg->VERSION()";
$pkgStatus{Perl}{pkgs}{$pkg}{status} =
'installed';
$pkgStatus{Perl}{installed}{$pkg} =
$v ? $v : 0;
push @{ $pkgStatus{Perl}{pkgs}{$missing}
{ $type . 's' }{$mAttr} },
$modName
unless (
grep ( /^$modName$/,
@{
$pkgStatus{Perl}{pkgs}{$missing}
{ $type . 's' }{$mAttr}
} )
);
# Lets also update the module meta data
if ( $type eq 'module' ) {
$modMeta->{prereqs}
{runtime}{$mAttr}{$missing} = 0;
push @{ $modMeta->{prereqs}{runtime}
{x_inherited}{$missing} },
$pkg;
}
else {
$packages{$modName}{META}{prereqs}
{runtime}{$mAttr}{$missing} = 0;
push
@{ $packages{$modName}{META}{prereqs}
{runtime}{x_inherited}{$missing}
},
$pkg;
}
}
}
else {
$pkgStatus{Perl}{pkgs}{$pkg}{status} =
'installed';
my $v = eval "$pkg->VERSION()";
$pkgStatus{Perl}{installed}{$pkg} =
$v ? $v : 0;
}
}
# check for outdated version
if ( $pkgStatus{Perl}{pkgs}{$pkg}{status} eq 'installed'
|| $pkg eq 'perl' )
{
my $reqV = $modPreqs->{$mAttr}{$pkg};
my $instV = $pkgStatus{Perl}{installed}{$pkg};
if ( defined($reqV)
&& $reqV ne ''
&& $reqV ne '0'
&& defined($instV)
&& $instV ne ''
&& $instV ne '0' )
{
$reqV = version->parse($reqV)->numify;
$instV = version->parse($instV)->numify;
#TODO suport for version range:
# https://metacpan.org/pod/ \
# CPAN::Meta::Spec#Version-Range
if ( $reqV > 0 && $instV < $reqV ) {
$pkgStatus{Perl}{pkgs}{$pkg}{status} =
'outdated';
push
@{ $pkgStatus{Perl}{outdated}{$pkg} },
$reqV;
$pkgStatus{Perl}{analyzed} = 1
if ( $modMeta->{x_prereqs_src} ne
'META.json'
&& !$pkgStatus{Perl}{analyzed} );
}
}
}
$pkgStatus{Perl}{pkgs}{$pkg}{timestamp} = $t;
}
}
}
#TODO
# nodejs
# python
}
}
# build installation hash
foreach my $area ( keys %pkgStatus ) {
foreach my $t (qw(missing outdated)) {
if ( defined( $pkgStatus{$area}{$t} )
&& ref( $pkgStatus{$area}{$t} ) eq 'HASH'
&& scalar keys %{ $pkgStatus{$area}{$t} } > 0 )
{
foreach my $pkg ( keys %{ $pkgStatus{$area}{$t} } ) {
next
unless ( ref( $pkgStatus{$area}{$t}{$pkg} ) eq 'ARRAY' );
# detect minimum required version
# for missing and outdated packages
my $v = maxNum( 0, @{ $pkgStatus{$area}{$t}{$pkg} } );
$pkgStatus{$area}{$t}{$pkg} = $v;
if (
defined(
$pkgStatus{$area}{pkgs}{$pkg}{modules}{requires}
)
&& @{ $pkgStatus{$area}{pkgs}{$pkg}{modules}{requires} }
> 0
)
{
$pkgStatus{counter}{total}++;
$pkgStatus{counter}{$t}++;
$pkgStatus{counter}{required}{total}++;
$pkgStatus{counter}{required}{$t}++;
$pkgStatus{counter}{required}{$area}{total}++;
$pkgStatus{counter}{required}{$area}{$t}++;
$pkgStatus{counter}{$area}{total}++;
$pkgStatus{counter}{$area}{$t}++;
$pkgStatus{counter}{$area}{required}{total}++;
$pkgStatus{counter}{$area}{required}{$t}++;
$pkgStatus{required}{$area}{$pkg}{status} = $t;
$pkgStatus{required}{$area}{$pkg}{version} = $v;
$pkgStatus{required}{$area}{$pkg}{modules} =
$pkgStatus{$area}{pkgs}{$pkg}{modules}{requires};
# add other modules
if (
defined(
$pkgStatus{$area}{pkgs}{$pkg}{modules}
{recommends}
)
&& @{
$pkgStatus{$area}{pkgs}{$pkg}{modules}
{recommends}
} > 0
)
{
foreach my $modName (
@{
$pkgStatus{$area}{pkgs}{$pkg}{modules}
{recommends}
}
)
{
push
@{ $pkgStatus{required}{$area}{$pkg}{modules}
},
$modName
unless (
grep ( /^$modName$/,
@{
$pkgStatus{required}{$area}
{$pkg}{modules}
} )
);
}
}
if (
defined(
$pkgStatus{$area}{pkgs}{$pkg}{modules}{suggests}
)
&& @{
$pkgStatus{$area}{pkgs}{$pkg}{modules}{suggests}
} > 0
)
{
foreach my $modName (
@{
$pkgStatus{$area}{pkgs}{$pkg}{modules}
{suggests}
}
)
{
push
@{ $pkgStatus{required}{$area}{$pkg}{modules}
},
$modName
unless (
grep ( /^$modName$/,
@{
$pkgStatus{required}{$area}
{$pkg}{modules}
} )
);
}
}
}
elsif (
defined(
$pkgStatus{$area}{pkgs}{$pkg}{modules}{recommends}
)
&& @{ $pkgStatus{$area}{pkgs}{$pkg}{modules}{recommends}
} > 0
)
{
$pkgStatus{counter}{total}++;
$pkgStatus{counter}{$t}++;
$pkgStatus{counter}{recommended}{total}++;
$pkgStatus{counter}{recommended}{$t}++;
$pkgStatus{counter}{recommended}{$area}{total}++;
$pkgStatus{counter}{recommended}{$area}{$t}++;
$pkgStatus{counter}{$area}{total}++;
$pkgStatus{counter}{$area}{$t}++;
$pkgStatus{counter}{$area}{recommended}{total}++;
$pkgStatus{counter}{$area}{recommended}{$t}++;
$pkgStatus{recommended}{$area}{$pkg}{status} = $t;
$pkgStatus{recommended}{$area}{$pkg}{version} = $v;
$pkgStatus{recommended}{$area}{$pkg}{modules} =
$pkgStatus{$area}{pkgs}{$pkg}{modules}{recommends};
# add other modules
if (
defined(
$pkgStatus{$area}{pkgs}{$pkg}{modules}{suggests}
)
&& @{
$pkgStatus{$area}{pkgs}{$pkg}{modules}{suggests}
} > 0
)
{
foreach my $modName (
@{
$pkgStatus{$area}{pkgs}{$pkg}{modules}
{suggests}
}
)
{
push @{ $pkgStatus{recommended}{$area}{$pkg}
{modules} },
$modName
unless (
grep ( /^$modName$/,
@{
$pkgStatus{recommended}{$area}
{$pkg}{modules}
} )
);
}
}
}
elsif (
defined(
$pkgStatus{$area}{pkgs}{$pkg}{modules}{suggests}
)
&& @{ $pkgStatus{$area}{pkgs}{$pkg}{modules}{suggests} }
> 0
)
{
$pkgStatus{counter}{total}++;
$pkgStatus{counter}{$t}++;
$pkgStatus{counter}{suggested}{total}++;
$pkgStatus{counter}{suggested}{$t}++;
$pkgStatus{counter}{suggested}{$area}{total}++;
$pkgStatus{counter}{suggested}{$area}{$t}++;
$pkgStatus{counter}{$area}{total}++;
$pkgStatus{counter}{$area}{$t}++;
$pkgStatus{counter}{$area}{suggested}{total}++;
$pkgStatus{counter}{$area}{suggested}{$t}++;
$pkgStatus{suggested}{$area}{$pkg}{status} = $t;
$pkgStatus{suggested}{$area}{$pkg}{version} = $v;
$pkgStatus{suggested}{$area}{$pkg}{modules} =
$pkgStatus{$area}{pkgs}{$pkg}{modules}{suggests};
}
}
}
else {
$pkgStatus{counter}{$t} = 0;
$pkgStatus{counter}{required}{$t} = 0;
$pkgStatus{counter}{required}{$area}{$t} = 0;
$pkgStatus{counter}{recommended}{$t} = 0;
$pkgStatus{counter}{recommended}{$area}{$t} = 0;
$pkgStatus{counter}{suggested}{$t} = 0;
$pkgStatus{counter}{suggested}{$area}{$t} = 0;
$pkgStatus{counter}{$area}{$t} = 0;
$pkgStatus{counter}{$area}{required}{$t} = 0;
$pkgStatus{counter}{$area}{recommended}{$t} = 0;
$pkgStatus{counter}{$area}{suggested}{$t} = 0;
}
}
}
if (@rets) {
$@ = join( "\n", @rets );
return "$@";
}
return undef;
}
#TODO
# Checks whether a NodeJS package is installed in the system
sub __IsInstalledNodejs($) {
return 0 unless ( __PACKAGE__ eq caller(0) );
return 0 unless (@_);
my ($pkg) = @_;
return 0;
}
#TODO
# Checks whether a Python package is installed in the system
sub __IsInstalledPython($) {
return 0 unless ( __PACKAGE__ eq caller(0) );
return 0 unless (@_);
my ($pkg) = @_;
return 0;
}
sub __GetExtendedEnvPath {
return 0 unless ( __PACKAGE__ eq caller(0) );
my @binpath = (
$Config::Config{'installsitebin'},
$Config::Config{'installsitescript'},
$Config::Config{'sitebin'},
$Config::Config{'sitescript'},
$Config::Config{'installscript'},
$Config::Config{'installvendorbin'},
$Config::Config{'installvendorscript'},
$Config::Config{'initialinstalllocation'},
$Config::Config{'bin'}
);
my @path = split( /:/, $ENV{PATH} );
foreach my $p ( reverse @binpath ) {
next unless ( $p && $p ne '' );
$p =~ s/\\/\\\\/g if ( $^O =~ m/Win/ );
unshift @path, $p unless ( grep ( /^$p$/, @path ) );
}
return join( ":", @path );
}
sub __ToDay() {
my ( $sec, $min, $hour, $mday, $month, $year, $wday, $yday, $isdst ) =
localtime( gettimeofday() );
$month++;
$year += 1900;
my $today = sprintf( '%04d-%02d-%02d', $year, $month, $mday );
return $today;
}
sub __aUniq {
my %seen;
grep !$seen{$_}++, @_;
}
use B qw/svref_2object/;
sub __in_package {
return 0 unless ( __PACKAGE__ eq caller(0) );
my ( $coderef, $package ) = @_;
my $cv = svref_2object($coderef);
return if not $cv->isa('B::CV') or $cv->GV->isa('B::SPECIAL');
return $cv->GV->STASH->NAME eq $package;
}
sub __list_module {
return 0 unless ( __PACKAGE__ eq caller(0) );
my $module = shift;
my $inPackage = shift;
no strict 'refs';
return grep {
defined &{"$module\::$_"}
and ( $inPackage ? __in_package( \&{*$_}, $module ) : 1 )
}
keys %{"$module\::"};
}
1;
=pod
=encoding utf8
=item helper
=item summary Module to help with FHEM installations
=item summary_DE Modul zur Unterstuetzung bei FHEM Installationen
=begin html
<a name="Installer" id="Installer"></a>
<h3>
Installer
</h3>
<ul>
<u><strong>Installer - Module to update FHEM, install 3rd-party FHEM modules and manage system prerequisites</strong></u><br />
<br />
<br />
<a name="Installerdefine" id="Installerdefine"></a><strong>Define</strong><br />
<ul>
<code>define &lt;name&gt; Installer</code><br />
<br />
Example:<br />
<ul>
<code>define fhemInstaller Installer</code><br />
</ul><br />
</ul><br />
<br />
<a name="Installerget" id="Installerget"></a><strong>Get</strong>
<ul>
<li>checkPrereqs - list all missing prerequisites. If no parameter was given, the running live system will be inspected. If the parameter is a FHEM cfg file, inspection will be based on devices from this file. If the parameter is a list of module names, those will be used for inspection.
</li>
<li>search - search FHEM for device names, module names, package names, keywords, authors and Perl package names.
</li>
<li>showModuleInfo - list information about a specific FHEM module
</li>
<li>showPackageInfo - list information about a specific FHEM package
</li>
<li>zzGetModuleMETA.json - prints raw meta information of a FHEM module in JSON format
</li>
<li>zzGetPackageMETA.json - prints raw meta information of a FHEM package in JSON format
</li>
</ul><br />
<br />
<a name="Installerattribut" id="Installerattribut"></a><strong>Attributes</strong>
<ul>
<li>disable - disables the device
</li>
<li>disabledForIntervals - disable device for interval time (13:00-18:30 or 13:00-18:30 22:00-23:00)
</li>
<li>installerMode - sets the installation mode. May be update, developer or install with update being the default setting. Some get and/or set commands may be hidden or limited depending on this.
</li>
</ul>
</ul>
=end html
=begin html_DE
<p>
<a name="Installer" id="Installer"></a>
</p>
<h3>
Installer
</h3>
<ul>
Eine deutsche Version der Dokumentation ist derzeit nicht vorhanden. Die englische Version ist hier zu finden:
</ul>
<ul>
<a href='http://fhem.de/commandref.html#Installer'>Installer</a>
</ul>
=end html_DE
=for :application/json;q=META.json 98_Installer.pm
{
"abstract": "Module to update FHEM, install 3rd-party FHEM modules and manage system prerequisites",
"x_lang": {
"de": {
"abstract": "Modul zum Update von FHEM, zur Installation von Drittanbieter FHEM Modulen und der Verwaltung von Systemvoraussetzungen"
}
},
"version": "v0.5.7",
"release_status": "stable",
"author": [
"Julian Pawlowski <julian.pawlowski@gmail.com>"
],
"x_fhem_maintainer": [
"loredo"
],
"x_fhem_maintainer_github": [
"jpawlowski"
],
"keywords": [
"Dependencies",
"Prerequisites",
"Setup"
],
"prereqs": {
"runtime": {
"requires": {
"Data::Dumper": 0,
"Encode": 0,
"FHEM": 5.00918623,
"FHEM::Meta": 0.001006,
"FHEM::npmjs": 0,
"File::stat": 0,
"GPUtils": 0,
"HttpUtils": 0,
"IO::Socket::SSL": 0,
"Config": 0,
"ExtUtils::Installed": 0,
"B": 0,
"JSON::PP": 0,
"perl": 5.014,
"version": 0,
"SubProcess": 0
},
"recommends": {
"Perl::PrereqScanner::NotQuiteLite": 0,
"JSON": 0
},
"suggests": {
"Cpanel::JSON::XS": 0,
"JSON::XS": 0
}
}
},
"resources": {
"bugtracker": {
"web": "https://github.com/fhem/Installer/issues"
}
}
}
=end :application/json;q=META.json
=cut