mirror of
https://github.com/fhem/fhem-mirror.git
synced 2025-01-31 18:59:33 +00:00
296a1fa7f6
git-svn-id: https://svn.fhem.de/fhem/trunk@28622 2b470e98-0d58-463d-a4d8-8e2adae1ed80
5608 lines
184 KiB
Perl
5608 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
|
|
)
|
|
);
|
|
}
|
|
|
|
use constant HAS_METARequirementsRange => defined eval {
|
|
require CPAN::Meta::Requirements::Range;
|
|
};
|
|
|
|
# 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 ? ' ' : ' ';
|
|
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 ? ' ' : ' ';
|
|
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 ? ' ' : ' ';
|
|
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 ? ' ' : ' ';
|
|
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 ? ' ' : ' ';
|
|
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}
|
|
. '">← 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}
|
|
. '">← 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 ? ' ' : ' ';
|
|
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' )
|
|
{
|
|
if ( !HAS_METARequirementsRange )
|
|
{
|
|
$pkgStatus{Perl}{pkgs}{$pkg}{status} = 'unknown';
|
|
next;
|
|
}
|
|
my $vRange = CPAN::Meta::Requirements::Range->with_string_requirement($reqV);
|
|
|
|
if (!defined $vRange->accepts($instV) )
|
|
{
|
|
$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 <name> 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.6.0",
|
|
"release_status": "stable",
|
|
"author": [
|
|
"Julian Pawlowski <julian.pawlowski@gmail.com>"
|
|
],
|
|
"x_fhem_maintainer": [
|
|
"coolTux"
|
|
],
|
|
"x_fhem_maintainer_github": [
|
|
"jpawlowski"
|
|
],
|
|
"keywords": [
|
|
"Dependencies",
|
|
"Prerequisites",
|
|
"Setup"
|
|
],
|
|
"prereqs": {
|
|
"runtime": {
|
|
"requires": {
|
|
"CPAN::Meta::Requirements::Range": "2.143",
|
|
"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
|