mirror of
https://github.com/fhem/fhem-mirror.git
synced 2025-05-04 22:19:38 +00:00
1787 lines
53 KiB
Perl
1787 lines
53 KiB
Perl
# $Id$
|
|
|
|
package main;
|
|
use strict;
|
|
use warnings;
|
|
use FHEM::Meta;
|
|
|
|
sub npmjs_Initialize($) {
|
|
my ($modHash) = @_;
|
|
|
|
$modHash->{SetFn} = "FHEM::npmjs::Set";
|
|
$modHash->{GetFn} = "FHEM::npmjs::Get";
|
|
$modHash->{DefFn} = "FHEM::npmjs::Define";
|
|
$modHash->{NotifyFn} = "FHEM::npmjs::Notify";
|
|
$modHash->{UndefFn} = "FHEM::npmjs::Undef";
|
|
$modHash->{AttrFn} = "FHEM::npmjs::Attr";
|
|
$modHash->{AttrList} =
|
|
"disable:1,0 "
|
|
. "disabledForIntervals "
|
|
. "updateListReading:1,0 "
|
|
. "npmglobal:1,0 "
|
|
. $readingFnAttributes;
|
|
|
|
return FHEM::Meta::Load( __FILE__, $modHash );
|
|
}
|
|
|
|
# define package
|
|
package FHEM::npmjs;
|
|
use strict;
|
|
use warnings;
|
|
use POSIX;
|
|
use FHEM::Meta;
|
|
|
|
use GPUtils qw(GP_Import);
|
|
use JSON;
|
|
use Data::Dumper;
|
|
|
|
# Run before module compilation
|
|
BEGIN {
|
|
# Import from main::
|
|
GP_Import(
|
|
qw(readingsSingleUpdate
|
|
readingsBulkUpdate
|
|
readingsBulkUpdateIfChanged
|
|
readingsBeginUpdate
|
|
readingsEndUpdate
|
|
ReadingsTimestamp
|
|
defs
|
|
modules
|
|
Log3
|
|
Debug
|
|
DoTrigger
|
|
CommandAttr
|
|
attr
|
|
AttrVal
|
|
ReadingsVal
|
|
Value
|
|
IsDisabled
|
|
deviceEvents
|
|
init_done
|
|
gettimeofday
|
|
InternalTimer
|
|
RemoveInternalTimer)
|
|
);
|
|
}
|
|
|
|
my %fhem_npm_modules = (
|
|
'alexa-fhem' => { fhem_module => 'alexa', },
|
|
'gassistant-fhem' => { fhem_module => 'gassistant', },
|
|
'homebridge-fhem' => { fhem_module => 'siri', },
|
|
'tradfri-fhem' => { fhem_module => 'tradfri', },
|
|
);
|
|
|
|
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->{HOST} = $host;
|
|
$hash->{NOTIFYDEV} = "global,$name";
|
|
|
|
return "Existing instance for host $hash->{HOST}: "
|
|
. $modules{ $hash->{TYPE} }{defptr}{ $hash->{HOST} }{NAME}
|
|
if ( defined( $modules{ $hash->{TYPE} }{defptr}{ $hash->{HOST} } ) );
|
|
|
|
$modules{ $hash->{TYPE} }{defptr}{ $hash->{HOST} } = $hash;
|
|
|
|
if ( $init_done && !defined( $hash->{OLDDEF} ) ) {
|
|
|
|
# presets for FHEMWEB
|
|
$attr{$name}{alias} = 'Node.js Package Update Status';
|
|
$attr{$name}{devStateIcon} =
|
|
'npm.updates.available:security@red:outdated npm.is.up.to.date:security@green:outdated .*npm.outdated.*in.progress:system_fhem_reboot@orange .*in.progress:system_fhem_update@orange warning.*:message_attention@orange error.*:message_attention@red';
|
|
$attr{$name}{group} = 'System';
|
|
$attr{$name}{icon} = 'npm-old';
|
|
$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{npmjs}{defptr}{ $hash->{HOST} } );
|
|
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, "npmjs ($name) - disabled";
|
|
}
|
|
|
|
elsif ( $cmd eq "del" ) {
|
|
Log3 $name, 3, "npmjs ($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, "npmjs ($name) - disabledForIntervals";
|
|
readingsSingleUpdate( $hash, "state", "disabled", 1 );
|
|
}
|
|
|
|
elsif ( $cmd eq "del" ) {
|
|
Log3 $name, 3, "npmjs ($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, "npmjs ($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'
|
|
)
|
|
)
|
|
{
|
|
|
|
# restore from packageList
|
|
my $decode_json =
|
|
eval { decode_json( ReadingsVal( $name, '.packageList', '' ) ) };
|
|
unless ($@) {
|
|
$hash->{".fhem"}{npm}{nodejsversions} = $decode_json->{versions}
|
|
if ( defined( $decode_json->{versions} ) );
|
|
$hash->{".fhem"}{npm}{listedpackages} = $decode_json->{listed}
|
|
if ( defined( $decode_json->{listed} ) );
|
|
$hash->{".fhem"}{npm}{outdatedpackages} = $decode_json->{outdated}
|
|
if ( defined( $decode_json->{outdated} ) );
|
|
}
|
|
$decode_json = undef;
|
|
|
|
# restore from installedList
|
|
$decode_json =
|
|
eval { decode_json( ReadingsVal( $name, '.installedList', '' ) ) };
|
|
unless ($@) {
|
|
$hash->{".fhem"}{npm}{installedpackages} = $decode_json;
|
|
}
|
|
$decode_json = undef;
|
|
|
|
# restore from uninstalledList
|
|
$decode_json =
|
|
eval { decode_json( ReadingsVal( $name, '.uninstalledList', '' ) ) };
|
|
unless ($@) {
|
|
$hash->{".fhem"}{npm}{uninstalledpackages} = $decode_json;
|
|
}
|
|
$decode_json = undef;
|
|
|
|
# restore from updatedList
|
|
$decode_json =
|
|
eval { decode_json( ReadingsVal( $name, '.updatedList', '' ) ) };
|
|
unless ($@) {
|
|
$hash->{".fhem"}{npm}{updatedpackages} = $decode_json;
|
|
}
|
|
$decode_json = undef;
|
|
|
|
# Trigger update
|
|
if ( ReadingsVal( $name, 'nodejsVersion', 'none' ) ne 'none' ) {
|
|
ProcessUpdateTimer($hash);
|
|
}
|
|
else {
|
|
$hash->{".fhem"}{npm}{cmd} = 'getNodeVersion';
|
|
AsynchronousExecuteNpmCommand($hash);
|
|
}
|
|
}
|
|
|
|
if (
|
|
$devname eq $name
|
|
and ( grep ( /^installed:.successful$/, @{$events} )
|
|
or grep ( /^uninstalled:.successful$/, @{$events} )
|
|
or grep ( /^updated:.successful$/, @{$events} ) )
|
|
)
|
|
{
|
|
$hash->{".fhem"}{npm}{cmd} = 'outdated';
|
|
AsynchronousExecuteNpmCommand($hash);
|
|
}
|
|
|
|
return;
|
|
}
|
|
|
|
sub Set($$@) {
|
|
|
|
my ( $hash, $name, @aa ) = @_;
|
|
|
|
my ( $cmd, @args ) = @aa;
|
|
|
|
if ( $cmd eq 'outdated' ) {
|
|
$hash->{".fhem"}{npm}{cmd} = $cmd;
|
|
}
|
|
elsif ( $cmd eq 'update' ) {
|
|
if ( defined( $args[0] ) and ( lc( $args[0] ) eq "fhem-all" ) ) {
|
|
return "Please run outdated check first"
|
|
unless ( defined( $hash->{".fhem"}{npm}{outdatedpackages} ) );
|
|
|
|
my $update;
|
|
foreach ( keys %fhem_npm_modules ) {
|
|
next
|
|
unless (
|
|
defined( $hash->{".fhem"}{npm}{outdatedpackages}{$_} ) );
|
|
$update .= " " if ($update);
|
|
$update .= $_;
|
|
}
|
|
return "No FHEM specific NPM modules left to update"
|
|
unless ($update);
|
|
$hash->{".fhem"}{npm}{cmd} = $cmd . " " . $update;
|
|
}
|
|
elsif ( defined( $args[0] ) and $args[0] eq "all" ) {
|
|
$hash->{".fhem"}{npm}{cmd} = $cmd;
|
|
}
|
|
else {
|
|
$hash->{".fhem"}{npm}{cmd} = $cmd . " " . join( " ", @args );
|
|
}
|
|
}
|
|
elsif ( $cmd eq 'install' ) {
|
|
return "usage: $cmd <package>" if ( @args < 1 );
|
|
if ( defined( $args[0] )
|
|
and ( lc( $args[0] ) eq "all" or lc( $args[0] ) eq "fhem-all" ) )
|
|
{
|
|
my $install;
|
|
foreach ( keys %fhem_npm_modules ) {
|
|
next
|
|
if (
|
|
defined(
|
|
$hash->{".fhem"}{npm}{listedpackages}{dependencies}{$_}
|
|
)
|
|
);
|
|
$install .= " " if ($install);
|
|
$install .= $_;
|
|
}
|
|
return "No FHEM specific NPM modules left to install"
|
|
unless ($install);
|
|
$hash->{".fhem"}{npm}{cmd} = $cmd . " " . $install;
|
|
}
|
|
else {
|
|
$hash->{".fhem"}{npm}{cmd} = $cmd . " " . join( " ", @args );
|
|
}
|
|
}
|
|
elsif ( $cmd eq 'uninstall' ) {
|
|
return "usage: $cmd <package>" if ( @args < 1 );
|
|
if ( defined( $args[0] ) and lc( $args[0] ) eq "fhem-all" ) {
|
|
my $uninstall;
|
|
foreach ( keys %fhem_npm_modules ) {
|
|
next
|
|
unless (
|
|
defined(
|
|
$hash->{".fhem"}{npm}{listedpackages}{dependencies}{$_}
|
|
)
|
|
);
|
|
$uninstall .= " " if ($uninstall);
|
|
$uninstall .= $_;
|
|
}
|
|
return "No FHEM specific NPM modules left to uninstall"
|
|
unless ($uninstall);
|
|
$hash->{".fhem"}{npm}{cmd} = $cmd . " " . $uninstall;
|
|
}
|
|
elsif ( defined( $args[0] ) and lc( $args[0] ) eq "all" ) {
|
|
return "Please run outdated check first"
|
|
unless ( defined( $hash->{".fhem"}{npm}{listedpackages} ) );
|
|
|
|
my $uninstall;
|
|
foreach (
|
|
keys %{ $hash->{".fhem"}{npm}{listedpackages}{dependencies} } )
|
|
{
|
|
next if ( $_ eq "npm" );
|
|
$uninstall .= " " if ($uninstall);
|
|
$uninstall .= $_;
|
|
}
|
|
return "There is nothing to uninstall"
|
|
unless ($uninstall);
|
|
$hash->{".fhem"}{npm}{cmd} = $cmd . " " . $uninstall;
|
|
}
|
|
else {
|
|
return "NPM cannot be uninstalled from here"
|
|
if (
|
|
grep ( m/^(?:@([\w-]+)\/)?(npm)(?:@([\d\.=<>]+|latest))?$/i,
|
|
@args ) );
|
|
$hash->{".fhem"}{npm}{cmd} = $cmd . " " . join( " ", @args );
|
|
}
|
|
}
|
|
else {
|
|
my $list = "";
|
|
|
|
if ( !defined( $hash->{".fhem"}{npm}{nodejsversions} ) ) {
|
|
$list =
|
|
"install:nodejs-v11,nodejs-v10,nodejs-v8,nodejs-v6,statusRequest";
|
|
}
|
|
else {
|
|
$list = "outdated:noArg";
|
|
|
|
my $install;
|
|
foreach ( keys %fhem_npm_modules ) {
|
|
next
|
|
if (
|
|
defined( $hash->{".fhem"}{npm}{listedpackages} )
|
|
and defined(
|
|
$hash->{".fhem"}{npm}{listedpackages}{dependencies}
|
|
)
|
|
and defined(
|
|
$hash->{".fhem"}{npm}{listedpackages}{dependencies}{$_}
|
|
)
|
|
);
|
|
$install .= "," if ($install);
|
|
$install = "install:fhem-all," unless ($install);
|
|
$install .= $_;
|
|
}
|
|
$install = "install" unless ($install);
|
|
$list .= " $install";
|
|
|
|
if ( defined( $hash->{".fhem"}{npm}{listedpackages} )
|
|
and
|
|
defined( $hash->{".fhem"}{npm}{listedpackages}{dependencies} )
|
|
and scalar
|
|
keys %{ $hash->{".fhem"}{npm}{listedpackages}{dependencies} } >
|
|
1 )
|
|
{
|
|
my $uninstall;
|
|
foreach (
|
|
sort
|
|
keys %{ $hash->{".fhem"}{npm}{listedpackages}{dependencies}
|
|
}
|
|
)
|
|
{
|
|
next if ( $_ eq "npm" or $_ eq "undefined" );
|
|
$uninstall .= "," if ($uninstall);
|
|
$uninstall = "uninstall:all,fhem-all,"
|
|
unless ($uninstall);
|
|
$uninstall .= $_;
|
|
}
|
|
$list .= " $uninstall";
|
|
}
|
|
|
|
if ( defined( $hash->{".fhem"}{npm}{outdatedpackages} )
|
|
and scalar
|
|
keys %{ $hash->{".fhem"}{npm}{outdatedpackages} } > 0 )
|
|
{
|
|
my $update;
|
|
foreach (
|
|
sort
|
|
keys %{ $hash->{".fhem"}{npm}{outdatedpackages} }
|
|
)
|
|
{
|
|
$update .= "," if ($update);
|
|
$update = "update:all,fhem-all," unless ($update);
|
|
$update .= $_;
|
|
}
|
|
$list .= " $update";
|
|
}
|
|
}
|
|
|
|
return "Unknown argument $cmd, choose one of $list";
|
|
}
|
|
|
|
AsynchronousExecuteNpmCommand($hash);
|
|
|
|
return undef;
|
|
}
|
|
|
|
sub Get($$@) {
|
|
|
|
my ( $hash, $name, @aa ) = @_;
|
|
|
|
my ( $cmd, @args ) = @aa;
|
|
|
|
if ( $cmd eq 'showOutdatedList' ) {
|
|
return "usage: $cmd" if ( @args != 0 );
|
|
|
|
my $ret = CreateOutdatedList( $hash, $cmd );
|
|
return $ret;
|
|
|
|
}
|
|
elsif ( $cmd eq 'showInstalledList' ) {
|
|
return "usage: $cmd" if ( @args != 0 );
|
|
|
|
my $ret = CreateInstalledList( $hash, $cmd );
|
|
return $ret;
|
|
|
|
}
|
|
|
|
# elsif ( $cmd eq 'showInstallResultList' ) {
|
|
# return "usage: $cmd" if ( @args != 0 );
|
|
#
|
|
# my $ret = CreateInstallResultList( $hash, $cmd );
|
|
# return $ret;
|
|
#
|
|
# }
|
|
# elsif ( $cmd eq 'showUninstallResultList' ) {
|
|
# return "usage: $cmd" if ( @args != 0 );
|
|
#
|
|
# my $ret = CreateUninstallResultList( $hash, $cmd );
|
|
# return $ret;
|
|
#
|
|
# }
|
|
# elsif ( $cmd eq 'showUpdateResultList' ) {
|
|
# return "usage: $cmd" if ( @args != 0 );
|
|
#
|
|
# my $ret = CreateUpdateResultList( $hash, $cmd );
|
|
# return $ret;
|
|
#
|
|
# }
|
|
# elsif ( $cmd eq 'showWarningList' ) {
|
|
# return "usage: $cmd" if ( @args != 0 );
|
|
#
|
|
# my $ret = CreateWarningList($hash);
|
|
# return $ret;
|
|
#
|
|
# }
|
|
elsif ( $cmd eq 'showErrorList' ) {
|
|
return "usage: $cmd" if ( @args != 0 );
|
|
|
|
my $ret = CreateErrorList($hash);
|
|
return $ret;
|
|
}
|
|
else {
|
|
my $list = "";
|
|
$list .= " showOutdatedList:noArg"
|
|
if ( defined( $hash->{".fhem"}{npm}{outdatedpackages} )
|
|
and scalar keys %{ $hash->{".fhem"}{npm}{outdatedpackages} } > 0 );
|
|
$list .= " showInstalledList:noArg"
|
|
if ( defined( $hash->{".fhem"}{npm}{listedpackages} )
|
|
and defined( $hash->{".fhem"}{npm}{listedpackages}{dependencies} )
|
|
and scalar
|
|
keys %{ $hash->{".fhem"}{npm}{listedpackages}{dependencies} } > 0 );
|
|
|
|
# $list .= " showInstallResultList:noArg"
|
|
# if ( defined( $hash->{".fhem"}{npm}{installedpackages} )
|
|
# and scalar keys %{ $hash->{".fhem"}{npm}{installedpackages} } > 0 );
|
|
# $list .= " showUninstallResultList:noArg"
|
|
# if ( defined( $hash->{".fhem"}{npm}{uninstalledpackages} )
|
|
# and scalar
|
|
# keys %{ $hash->{".fhem"}{npm}{uninstalledpackages} } > 0 );
|
|
# $list .= " showUpdateResultList:noArg"
|
|
# if ( defined( $hash->{".fhem"}{npm}{updatedpackages} )
|
|
# and scalar keys %{ $hash->{".fhem"}{npm}{updatedpackages} } > 0 );
|
|
# $list .= " showWarningList:noArg"
|
|
# if ( defined( $hash->{".fhem"}{npm}{'warnings'} )
|
|
# and scalar keys %{ $hash->{".fhem"}{npm}{'warnings'} } > 0 );
|
|
|
|
$list .= " showErrorList:noArg"
|
|
if ( defined( $hash->{".fhem"}{npm}{errors} )
|
|
and scalar keys %{ $hash->{".fhem"}{npm}{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"}{npm}{cmd} )
|
|
&& $hash->{".fhem"}{npm}{cmd} =~
|
|
m/^(install|uninstall|update)(?: (.+))/i );
|
|
|
|
my $cmd = $1;
|
|
my $packages = $2;
|
|
|
|
my $list;
|
|
|
|
foreach my $package ( split / /, $packages ) {
|
|
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::npmjs::ProcessUpdateTimer",
|
|
$hash, 0
|
|
);
|
|
Log3 $name, 4, "npmjs ($name) - stateRequestTimer: Call Request Timer";
|
|
|
|
unless ( IsDisabled($name) ) {
|
|
if ( exists( $hash->{".fhem"}{subprocess} ) ) {
|
|
Log3 $name, 2,
|
|
"npmjs ($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, 'outdated', '1970-01-01' )
|
|
)
|
|
)[0]
|
|
or ReadingsVal( $name, 'state', '' ) eq 'disabled'
|
|
)
|
|
{
|
|
$hash->{".fhem"}{npm}{cmd} = 'outdated';
|
|
AsynchronousExecuteNpmCommand($hash);
|
|
}
|
|
}
|
|
}
|
|
|
|
sub CleanSubprocess($) {
|
|
|
|
my $hash = shift;
|
|
|
|
my $name = $hash->{NAME};
|
|
|
|
delete( $hash->{".fhem"}{subprocess} );
|
|
Log3 $name, 4, "npmjs ($name) - clean Subprocess";
|
|
}
|
|
|
|
use constant POLLINTERVAL => 1;
|
|
|
|
sub AsynchronousExecuteNpmCommand($) {
|
|
|
|
require "SubProcess.pm";
|
|
my ($hash) = shift;
|
|
|
|
my $name = $hash->{NAME};
|
|
|
|
my $subprocess = SubProcess->new( { onRun => \&OnRun } );
|
|
$subprocess->{npm} = $hash->{".fhem"}{npm};
|
|
$subprocess->{npm}{host} = $hash->{HOST};
|
|
$subprocess->{npm}{debug} =
|
|
( AttrVal( $name, 'verbose', 0 ) > 3 ? 1 : 0 );
|
|
$subprocess->{npm}{npmglobal} =
|
|
( AttrVal( $name, 'npmglobal', 1 ) == 1 ? 1 : 0 );
|
|
my $pid = $subprocess->run();
|
|
|
|
readingsSingleUpdate( $hash, 'state',
|
|
'command \'npm ' . $hash->{".fhem"}{npm}{cmd} . '\' in progress', 1 );
|
|
|
|
if ( !defined($pid) ) {
|
|
Log3 $name, 1, "npmjs ($name) - Cannot execute command asynchronously";
|
|
|
|
CleanSubprocess($hash);
|
|
readingsSingleUpdate( $hash, 'state',
|
|
'Cannot execute command asynchronously', 1 );
|
|
return undef;
|
|
}
|
|
|
|
Event( $hash, "BEGIN" );
|
|
Log3 $name, 4, "npmjs ($name) - execute command asynchronously (PID= $pid)";
|
|
|
|
$hash->{".fhem"}{subprocess} = $subprocess;
|
|
|
|
InternalTimer( gettimeofday() + POLLINTERVAL,
|
|
"FHEM::npmjs::PollChild", $hash, 0 );
|
|
Log3 $hash, 4, "npmjs ($name) - control passed back to main loop.";
|
|
}
|
|
|
|
sub PollChild($) {
|
|
|
|
my $hash = shift;
|
|
|
|
my $name = $hash->{NAME};
|
|
my $subprocess = $hash->{".fhem"}{subprocess};
|
|
my $json = $subprocess->readFromChild();
|
|
|
|
if ( !defined($json) ) {
|
|
Log3 $name, 5,
|
|
"npmjs ($name) - still waiting (" . $subprocess->{lasterror} . ").";
|
|
InternalTimer( gettimeofday() + POLLINTERVAL,
|
|
"FHEM::npmjs::PollChild", $hash, 0 );
|
|
return;
|
|
}
|
|
else {
|
|
Log3 $name, 4, "npmjs ($name) - got result from asynchronous parsing.";
|
|
$subprocess->wait();
|
|
Log3 $name, 4, "npmjs ($name) - asynchronous finished.";
|
|
|
|
CleanSubprocess($hash);
|
|
PreProcessing( $hash, $json );
|
|
}
|
|
}
|
|
|
|
######################################
|
|
# Begin Childprocess
|
|
######################################
|
|
|
|
sub OnRun() {
|
|
my $subprocess = shift;
|
|
my $response = ExecuteNpmCommand( $subprocess->{npm} );
|
|
|
|
my $json = eval { encode_json($response) };
|
|
if ($@) {
|
|
Log3 'npmjs OnRun', 3, "npmjs - JSON error: $@";
|
|
$json = "{\"jsonerror\":\"$@\"}";
|
|
}
|
|
|
|
$subprocess->writeToParent($json);
|
|
}
|
|
|
|
sub ExecuteNpmCommand($) {
|
|
|
|
my $cmd = shift;
|
|
|
|
my $npm = {};
|
|
$npm->{debug} = $cmd->{debug};
|
|
|
|
my $cmdPrefix = "";
|
|
my $cmdSuffix = "";
|
|
|
|
if ( $cmd->{host} =~ /^(?:(.*)@)?([^:]+)(?::(\d+))?$/
|
|
&& lc($2) ne "localhost" )
|
|
{
|
|
my $port = "";
|
|
if ($3) {
|
|
$port = "-p $3 ";
|
|
}
|
|
|
|
# One-time action to add remote hosts key.
|
|
# If key changes, user will need to intervene
|
|
# and cleanup known_hosts file manually for security reasons
|
|
$cmdPrefix =
|
|
'KEY=$(ssh-keyscan -t ed25519 '
|
|
. $2
|
|
. ' 2>/dev/null); '
|
|
. 'grep -q -E "^${KEY% *}" ${HOME}/.ssh/known_hosts || echo "${KEY}" >> ${HOME}/.ssh/known_hosts; ';
|
|
$cmdPrefix .=
|
|
'KEY=$(ssh-keyscan -t rsa '
|
|
. $2
|
|
. ' 2>/dev/null); '
|
|
. 'grep -q -E "^${KEY% *}" ${HOME}/.ssh/known_hosts || echo "${KEY}" >> ${HOME}/.ssh/known_hosts; ';
|
|
|
|
# wrap SSH command
|
|
$cmdPrefix .=
|
|
'ssh -oBatchMode=yes ' . $port . ( $1 ? "$1@" : "" ) . $2 . ' \'';
|
|
$cmdSuffix = '\' 2>&1';
|
|
}
|
|
|
|
my $global = '-g ';
|
|
my $sudo = 'sudo -n -E ';
|
|
|
|
if ( $cmd->{npmglobal} eq '0' ) {
|
|
$global = '';
|
|
$sudo = '';
|
|
}
|
|
|
|
$npm->{nodejsversions} =
|
|
$cmdPrefix
|
|
. 'echo n | node -e "console.log(JSON.stringify(process.versions));" 2>&1'
|
|
. $cmdSuffix;
|
|
$npm->{npminstall} =
|
|
$cmdPrefix
|
|
. 'echo n | sh -c "'
|
|
. $sudo
|
|
. 'npm install '
|
|
. $global
|
|
. '--json --silent --unsafe-perm %PACKAGES%" 2>&1'
|
|
. $cmdSuffix;
|
|
$npm->{npmuninstall} =
|
|
$cmdPrefix
|
|
. 'echo n | sh -c "'
|
|
. $sudo
|
|
. 'npm uninstall '
|
|
. $global
|
|
. '--json --silent %PACKAGES%" 2>&1'
|
|
. $cmdSuffix;
|
|
$npm->{npmupdate} =
|
|
$cmdPrefix
|
|
. 'echo n | sh -c "'
|
|
. $sudo
|
|
. 'npm update '
|
|
. $global
|
|
. '--json --silent --unsafe-perm %PACKAGES%" 2>&1'
|
|
. $cmdSuffix;
|
|
$npm->{npmoutdated} =
|
|
$cmdPrefix
|
|
. 'echo n | '
|
|
. 'echo "{' . "\n"
|
|
. '\"versions\": "; '
|
|
. 'node -e "console.log(JSON.stringify(process.versions));"; '
|
|
. 'L1=$(npm list '
|
|
. $global
|
|
. '--json --silent --depth=0 2>/dev/null); '
|
|
. '[ "$L1" != "" ] && [ "$L1" != "\n" ] && echo ", \"listed\": $L1"; '
|
|
. 'L2=$(npm outdated '
|
|
. $global
|
|
. '--json --silent 2>&1); '
|
|
. '[ "$L2" != "" ] && [ "$L2" != "\n" ] && echo ", \"outdated\": $L2"; '
|
|
. 'echo "}"'
|
|
. $cmdSuffix;
|
|
|
|
my $response;
|
|
|
|
if ( $cmd->{cmd} =~ /^install (.+)/ ) {
|
|
if ( not defined( $cmd->{nodejsversions} )
|
|
or not defined( $cmd->{nodejsversions}{node} ) )
|
|
{
|
|
if ( lc($1) eq "statusrequest" ) {
|
|
$npm->{npminstall} = $npm->{nodejsversions};
|
|
}
|
|
elsif ( $1 =~ /^nodejs-v(\d+)/ ) {
|
|
$npm->{npminstall} =
|
|
$cmdPrefix
|
|
. 'echo n | if [ -z "$(node --version 2>/dev/null)" ]; then'
|
|
. ' sh -c "curl -sSL https://deb.nodesource.com/setup_'.$1.'.x | DEBIAN_FRONTEND=noninteractive sudo -n -E bash - >/dev/null 2>&1" 2>&1 &&'
|
|
. ' sh -c "DEBIAN_FRONTEND=noninteractive sudo -n -E apt-get install -qqy nodejs >/dev/null 2>&1" 2>&1; '
|
|
. 'fi; '
|
|
. 'node -e "console.log(JSON.stringify(process.versions));" 2>&1'
|
|
. $cmdSuffix;
|
|
}
|
|
}
|
|
else {
|
|
my @packages = "";
|
|
foreach my $package ( split / /, $1 ) {
|
|
next
|
|
unless ( $package =~
|
|
/^(?:@([\w-]+)\/)?([\w-]+)(?:@([\d\.=<>]+|latest))?$/ );
|
|
|
|
push @packages,
|
|
"homebridge"
|
|
if (
|
|
$package =~ m/^homebridge-/i
|
|
&& (
|
|
defined( $cmd->{listedpackages} )
|
|
and defined( $cmd->{listedpackages}{dependencies} )
|
|
and !defined(
|
|
$cmd->{listedpackages}{dependencies}{homebridge}
|
|
)
|
|
)
|
|
);
|
|
|
|
push @packages, $package;
|
|
}
|
|
my $pkglist = join( ' ', @packages );
|
|
return unless ( $pkglist ne "" );
|
|
$npm->{npminstall} =~ s/%PACKAGES%/$pkglist/gi;
|
|
}
|
|
print qq($npm->{npminstall}\n) if ( $npm->{debug} == 1 );
|
|
$response = NpmInstall($npm);
|
|
}
|
|
elsif ( $cmd->{cmd} =~ /^uninstall (.+)/ ) {
|
|
my @packages = "";
|
|
foreach my $package ( split / /, $1 ) {
|
|
next
|
|
unless ( $package =~
|
|
/^(?:@([\w-]+)\/)?([\w-]+)(?:@([\d\.=<>]+|latest))?$/ );
|
|
push @packages, $package;
|
|
}
|
|
my $pkglist = join( ' ', @packages );
|
|
return unless ( $pkglist ne "" );
|
|
$npm->{npmuninstall} =~ s/%PACKAGES%/$pkglist/gi;
|
|
print qq($npm->{npmuninstall}\n) if ( $npm->{debug} == 1 );
|
|
$response = NpmUninstall($npm);
|
|
}
|
|
elsif ( $cmd->{cmd} =~ /^update(?: (.+))?/ ) {
|
|
my $pkglist = "";
|
|
if ( defined($1) ) {
|
|
my @packages;
|
|
foreach my $package ( split / /, $1 ) {
|
|
next
|
|
unless ( $package =~
|
|
/^(?:@([\w-]+)\/)?([\w-]+)(?:@([\d\.=<>]+|latest))?$/ );
|
|
push @packages, $package;
|
|
}
|
|
$pkglist = join( ' ', @packages );
|
|
}
|
|
$npm->{npmupdate} =~ s/%PACKAGES%/$pkglist/gi;
|
|
print qq($npm->{npmupdate}\n) if ( $npm->{debug} == 1 );
|
|
$response = NpmUpdate($npm);
|
|
}
|
|
elsif ( $cmd->{cmd} eq 'outdated' ) {
|
|
print qq($npm->{npmoutdated}\n) if ( $npm->{debug} == 1 );
|
|
$response = NpmOutdated($npm);
|
|
}
|
|
elsif ( $cmd->{cmd} eq 'getNodeVersion' ) {
|
|
print qq($npm->{nodejsversions}\n) if ( $npm->{debug} == 1 );
|
|
$response = GetNodeVersion($npm);
|
|
}
|
|
|
|
return $response;
|
|
}
|
|
|
|
sub GetNodeVersion($) {
|
|
my $cmd = shift;
|
|
my $p = `$cmd->{nodejsversions}`;
|
|
my $ret = RetrieveNpmOutput( $cmd, $p );
|
|
|
|
return { versions => $ret }
|
|
if ( scalar keys %{$ret} > 0 && !defined( $ret->{error} ) );
|
|
return $ret;
|
|
}
|
|
|
|
sub NpmUninstall($) {
|
|
my $cmd = shift;
|
|
my $p = `$cmd->{npmuninstall}`;
|
|
my $ret = RetrieveNpmOutput( $cmd, $p );
|
|
|
|
return $ret;
|
|
}
|
|
|
|
sub NpmUpdate($) {
|
|
my $cmd = shift;
|
|
my $p = `$cmd->{npmupdate}`;
|
|
my $ret = RetrieveNpmOutput( $cmd, $p );
|
|
|
|
return $ret;
|
|
}
|
|
|
|
sub NpmInstall($) {
|
|
my $cmd = shift;
|
|
my $p = `$cmd->{npminstall}`;
|
|
my $ret = RetrieveNpmOutput( $cmd, $p );
|
|
|
|
# this will come back only after
|
|
# nodejs installation
|
|
return { versions => $ret }
|
|
if ( scalar keys %{$ret} > 0
|
|
&& defined( $ret->{node} ) );
|
|
return $ret;
|
|
}
|
|
|
|
sub NpmOutdated($) {
|
|
my $cmd = shift;
|
|
my $p = `$cmd->{npmoutdated}`;
|
|
my $ret = RetrieveNpmOutput( $cmd, $p );
|
|
|
|
return $ret;
|
|
}
|
|
|
|
sub RetrieveNpmOutput($$) {
|
|
my $cmd = shift;
|
|
my $p = shift;
|
|
my $h = {};
|
|
|
|
# first try to interprete text as JSON directly
|
|
my $decode_json = eval { decode_json($p) };
|
|
if ( not $@ ) {
|
|
$h = $decode_json;
|
|
}
|
|
|
|
# if this was not successful,
|
|
# we'll disassamble the text
|
|
else {
|
|
my $o;
|
|
my $json;
|
|
my $skip = 0;
|
|
|
|
foreach my $line ( split /\n/, $p ) {
|
|
chomp($line);
|
|
print qq($line\n) if ( $cmd->{debug} == 1 );
|
|
|
|
# JSON output
|
|
if ($skip) {
|
|
$json .= $line;
|
|
}
|
|
|
|
# reached JSON
|
|
elsif ( $line =~ /^\{$/ ) {
|
|
$json = $line;
|
|
$skip = 1;
|
|
}
|
|
|
|
# other output before JSON
|
|
else {
|
|
$o .= $line;
|
|
}
|
|
}
|
|
|
|
$decode_json = eval { decode_json($json) };
|
|
|
|
# Found valid JSON output
|
|
if ( not $@ ) {
|
|
$h = $decode_json;
|
|
}
|
|
|
|
# Final parsing error
|
|
else {
|
|
if ($o) {
|
|
if ( $o =~ m/Permission.denied.\(publickey\)\.?\r?\n?$/i ) {
|
|
$h->{error}{code} = "E403";
|
|
$h->{error}{summary} =
|
|
"Forbidden - None of the SSH keys from ~/.ssh/ "
|
|
. "were authorized to access remote host";
|
|
$h->{error}{detail} = $o;
|
|
}
|
|
elsif ( $o =~ m/^sudo: /i ) {
|
|
$h->{error}{code} = "E403";
|
|
$h->{error}{summary} =
|
|
"Forbidden - " . "passwordless sudo permissions required";
|
|
$h->{error}{detail} =
|
|
$o . "\n\n"
|
|
. "You may add the following lines to /etc/sudoers.d/fhem:\n"
|
|
. " fhem ALL=NOPASSWD: /usr/bin/npm update *\n"
|
|
. " fhem ALL=NOPASSWD: /usr/bin/npm install *\n"
|
|
. " fhem ALL=NOPASSWD: /usr/bin/npm uninstall *";
|
|
}
|
|
elsif ( $o =~
|
|
m/(?:(\w+?): )?(?:(\w+? \d+): )?(\w+?): [^:]*?not.found$/i
|
|
or $o =~
|
|
m/(?:(\w+?): )?(?:(\w+? \d+): )?(\w+?): [^:]*?No.such.file.or.directory$/i
|
|
)
|
|
{
|
|
$h->{error}{code} = "E404";
|
|
$h->{error}{summary} = "Not Found - $3 is not installed";
|
|
$h->{error}{detail} = $o;
|
|
}
|
|
else {
|
|
$h->{error}{code} = "E501";
|
|
$h->{error}{summary} = "Parsing error - " . $@;
|
|
$h->{error}{detail} = $p;
|
|
}
|
|
}
|
|
else {
|
|
$h->{error}{code} = "E500";
|
|
$h->{error}{summary} = "Parsing error - " . $@;
|
|
$h->{error}{detail} = $p;
|
|
}
|
|
}
|
|
}
|
|
|
|
return $h;
|
|
}
|
|
|
|
####################################################
|
|
# End Childprocess
|
|
####################################################
|
|
|
|
sub PreProcessing($$) {
|
|
|
|
my ( $hash, $json ) = @_;
|
|
|
|
my $name = $hash->{NAME};
|
|
|
|
my $decode_json = eval { decode_json($json) };
|
|
if ($@) {
|
|
Log3 $name, 2, "npmjs ($name) - JSON error: $@";
|
|
return;
|
|
}
|
|
|
|
Log3 $hash, 4, "npmjs ($name) - JSON: $json";
|
|
|
|
if ( defined( $decode_json->{versions} )
|
|
&& defined( $decode_json->{versions}{node} ) )
|
|
{
|
|
$hash->{".fhem"}{npm}{nodejsversions} = $decode_json->{versions};
|
|
}
|
|
|
|
# safe result in hidden reading
|
|
# to restore module state after reboot
|
|
if ( $hash->{".fhem"}{npm}{cmd} eq 'outdated' ) {
|
|
delete $hash->{".fhem"}{npm}{outdatedpackages};
|
|
$hash->{".fhem"}{npm}{outdatedpackages} = $decode_json->{outdated}
|
|
if ( defined( $decode_json->{outdated} ) );
|
|
delete $hash->{".fhem"}{npm}{listedpackages};
|
|
$hash->{".fhem"}{npm}{listedpackages} = $decode_json->{listed}
|
|
if ( defined( $decode_json->{listed} ) );
|
|
readingsSingleUpdate( $hash, '.packageList', $json, 0 );
|
|
}
|
|
elsif ( $hash->{".fhem"}{npm}{cmd} =~ /^install/ ) {
|
|
delete $hash->{".fhem"}{npm}{installedpackages};
|
|
$hash->{".fhem"}{npm}{installedpackages} = $decode_json;
|
|
readingsSingleUpdate( $hash, '.installedList', $json, 0 );
|
|
}
|
|
elsif ( $hash->{".fhem"}{npm}{cmd} =~ /^uninstall/ ) {
|
|
delete $hash->{".fhem"}{npm}{uninstalledpackages};
|
|
$hash->{".fhem"}{npm}{uninstalledpackages} = $decode_json;
|
|
readingsSingleUpdate( $hash, '.uninstalledList', $json, 0 );
|
|
}
|
|
elsif ( $hash->{".fhem"}{npm}{cmd} =~ /^update/ ) {
|
|
delete $hash->{".fhem"}{npm}{updatedpackages};
|
|
$hash->{".fhem"}{npm}{updatedpackages} = $decode_json;
|
|
readingsSingleUpdate( $hash, '.updatedList', $json, 0 );
|
|
}
|
|
|
|
if ( defined( $decode_json->{warning} )
|
|
or defined( $decode_json->{error} ) )
|
|
{
|
|
$hash->{".fhem"}{npm}{'warnings'} = $decode_json->{warning}
|
|
if ( defined( $decode_json->{warning} ) );
|
|
$hash->{".fhem"}{npm}{errors} = $decode_json->{error}
|
|
if ( defined( $decode_json->{error} ) );
|
|
}
|
|
else {
|
|
delete $hash->{".fhem"}{npm}{'warnings'};
|
|
delete $hash->{".fhem"}{npm}{errors};
|
|
}
|
|
|
|
WriteReadings( $hash, $decode_json );
|
|
}
|
|
|
|
sub WriteReadings($$) {
|
|
|
|
my ( $hash, $decode_json ) = @_;
|
|
|
|
my $name = $hash->{NAME};
|
|
|
|
Log3 $hash, 4, "npmjs ($name) - Write Readings";
|
|
Log3 $hash, 5, "npmjs ($name) - " . Dumper $decode_json;
|
|
|
|
readingsBeginUpdate($hash);
|
|
|
|
if ( $hash->{".fhem"}{npm}{cmd} eq 'outdated' ) {
|
|
readingsBulkUpdate(
|
|
$hash,
|
|
'outdated',
|
|
(
|
|
defined( $decode_json->{listed} )
|
|
? 'check completed'
|
|
: 'check failed'
|
|
)
|
|
);
|
|
delete $hash->{".fhem"}{npm}{nodejsversions}
|
|
unless ( $decode_json->{versions} );
|
|
$hash->{helper}{lastSync} = ToDay();
|
|
}
|
|
|
|
readingsBulkUpdateIfChanged( $hash, 'updatesAvailable',
|
|
scalar keys %{ $decode_json->{outdated} } )
|
|
if ( $hash->{".fhem"}{npm}{cmd} eq 'outdated' );
|
|
readingsBulkUpdateIfChanged( $hash, 'updateListAsJSON',
|
|
eval { encode_json( $hash->{".fhem"}{npm}{outdatedpackages} ) } )
|
|
if ( AttrVal( $name, 'updateListReading', 'none' ) ne 'none' );
|
|
|
|
my $result = 'successful';
|
|
$result = 'error' if ( defined( $hash->{".fhem"}{npm}{errors} ) );
|
|
$result = 'warning' if ( defined( $hash->{".fhem"}{npm}{'warnings'} ) );
|
|
|
|
readingsBulkUpdate( $hash, 'installed', $result )
|
|
if ( $hash->{".fhem"}{npm}{cmd} =~ /^install/ );
|
|
readingsBulkUpdate( $hash, 'uninstalled', $result )
|
|
if ( $hash->{".fhem"}{npm}{cmd} =~ /^uninstall/ );
|
|
readingsBulkUpdate( $hash, 'updated', $result )
|
|
if ( $hash->{".fhem"}{npm}{cmd} =~ /^update/ );
|
|
|
|
readingsBulkUpdateIfChanged( $hash, "nodejsVersion",
|
|
$decode_json->{versions}{node} )
|
|
if ( defined( $decode_json->{versions} )
|
|
&& defined( $decode_json->{versions}{node} ) );
|
|
|
|
if ( defined( $decode_json->{error} ) ) {
|
|
readingsBulkUpdate( $hash, 'state',
|
|
'error \'' . $hash->{".fhem"}{npm}{cmd} . '\'' );
|
|
}
|
|
elsif ( defined( $decode_json->{warning} ) ) {
|
|
readingsBulkUpdate( $hash, 'state',
|
|
'warning \'' . $hash->{".fhem"}{npm}{cmd} . '\'' );
|
|
}
|
|
else {
|
|
|
|
readingsBulkUpdate(
|
|
$hash, 'state',
|
|
(
|
|
(
|
|
scalar keys %{ $decode_json->{outdated} } > 0
|
|
or scalar
|
|
keys %{ $hash->{".fhem"}{npm}{outdatedpackages} } > 0
|
|
)
|
|
? 'npm updates available'
|
|
: 'npm is up to date'
|
|
)
|
|
);
|
|
}
|
|
|
|
Event( $hash, "FINISH" );
|
|
readingsEndUpdate( $hash, 1 );
|
|
|
|
ProcessUpdateTimer($hash)
|
|
if ( $hash->{".fhem"}{npm}{cmd} eq 'getNodeVersion'
|
|
&& !defined( $decode_json->{error} ) );
|
|
}
|
|
|
|
sub CreateWarningList($) {
|
|
|
|
my $hash = shift;
|
|
|
|
my $warnings = $hash->{".fhem"}{npm}{'warnings'};
|
|
|
|
my $ret = '<html><table><tr><td>';
|
|
$ret .= '<table class="block wide">';
|
|
$ret .= '<tr class="even">';
|
|
$ret .= "<td><b>Warning List</b></td>";
|
|
$ret .= "<td></td>";
|
|
$ret .= '</tr>';
|
|
|
|
if ( ref($warnings) eq "ARRAY" ) {
|
|
|
|
my $linecount = 1;
|
|
foreach my $warning ( @{$warnings} ) {
|
|
if ( $linecount % 2 == 0 ) {
|
|
$ret .= '<tr class="even">';
|
|
}
|
|
else {
|
|
$ret .= '<tr class="odd">';
|
|
}
|
|
|
|
$ret .= "<td>$warning->{message}</td>";
|
|
|
|
$ret .= '</tr>';
|
|
$linecount++;
|
|
}
|
|
}
|
|
|
|
$ret .= '</table></td></tr>';
|
|
$ret .= '</table></html>';
|
|
|
|
return $ret;
|
|
}
|
|
|
|
sub CreateErrorList($) {
|
|
my $hash = shift;
|
|
my $error = $hash->{".fhem"}{npm}{errors};
|
|
|
|
my $ret = '<html><table style="min-width: 450px;"><tr><td>';
|
|
$ret .= '<table class="block wide">';
|
|
|
|
if ( ref($error) eq "HASH" ) {
|
|
$ret .= '<tr class="even">';
|
|
$ret .= "<td><b>Error code $error->{code}</b></td>";
|
|
$ret .= "<td></td>";
|
|
$ret .= '</tr>';
|
|
$ret .= '<tr class="odd">';
|
|
$ret .= "<td>Summary:\n$error->{summary}\n\n</td>";
|
|
$ret .= '</tr>';
|
|
$ret .= '<tr class="even">';
|
|
$ret .= "<td>Detail:\n$error->{detail}</td>";
|
|
$ret .= '</tr>';
|
|
}
|
|
else {
|
|
$ret .= '<tr class="even">';
|
|
$ret .= "<td><b>Error List</b></td>";
|
|
$ret .= "<td></td>";
|
|
$ret .= '</tr>';
|
|
}
|
|
|
|
$ret .= '</table></td></tr>';
|
|
$ret .= '</table></html>';
|
|
|
|
return $ret;
|
|
}
|
|
|
|
sub CreateInstalledList($$) {
|
|
my ( $hash, $getCmd ) = @_;
|
|
my @ret;
|
|
my $packages;
|
|
my $html = defined( $hash->{CL} ) && $hash->{CL}{TYPE} eq "FHEMWEB" ? 1 : 0;
|
|
$packages = $hash->{".fhem"}{npm}{listedpackages}{dependencies};
|
|
|
|
my $header = "";
|
|
my $footer = "";
|
|
if ($html) {
|
|
$header = '<html><table style="min-width: 450px;" class="block wide">';
|
|
$footer = '</table></html>';
|
|
}
|
|
|
|
my $rowOpen = "";
|
|
my $rowOpenEven = "";
|
|
my $rowOpenOdd = "";
|
|
my $colOpen = "";
|
|
my $txtOpen = "";
|
|
my $txtClose = "";
|
|
my $colClose = "\t\t\t";
|
|
my $rowClose = "";
|
|
|
|
if ($html) {
|
|
$rowOpen = '<tr>';
|
|
$rowOpenEven = '<tr class="even">';
|
|
$rowOpenOdd = '<tr class="odd">';
|
|
$colOpen = '<td>';
|
|
$txtOpen = "<b>";
|
|
$txtClose = "</b>";
|
|
$colClose = '</td>';
|
|
$rowClose = '</tr>';
|
|
}
|
|
|
|
push @ret,
|
|
$rowOpen
|
|
. $colOpen
|
|
. $txtOpen
|
|
. 'Package Name'
|
|
. $txtClose
|
|
. $colClose
|
|
. $colOpen
|
|
. $txtOpen
|
|
. 'Current Version'
|
|
. $txtClose
|
|
. $colClose
|
|
. $rowClose;
|
|
|
|
if ( ref($packages) eq "HASH" ) {
|
|
|
|
my $linecount = 1;
|
|
foreach my $package ( sort keys( %{$packages} ) ) {
|
|
next if ( $package eq "undefined" );
|
|
|
|
my $l = $linecount % 2 == 0 ? $rowOpenEven : $rowOpenOdd;
|
|
$l .= $colOpen . $package . $colClose;
|
|
$l .= $colOpen
|
|
. (
|
|
defined( $packages->{$package}{version} )
|
|
? $packages->{$package}{version}
|
|
: '?'
|
|
) . $colClose;
|
|
$l .= $rowClose;
|
|
|
|
push @ret, $l;
|
|
$linecount++;
|
|
}
|
|
}
|
|
|
|
return $header . join( "\n", @ret ) . $footer;
|
|
}
|
|
|
|
sub CreateOutdatedList($$) {
|
|
my ( $hash, $getCmd ) = @_;
|
|
my @ret;
|
|
my $packages;
|
|
my $html = defined( $hash->{CL} ) && $hash->{CL}{TYPE} eq "FHEMWEB" ? 1 : 0;
|
|
$packages = $hash->{".fhem"}{npm}{outdatedpackages};
|
|
|
|
my $header = "";
|
|
my $footer = "";
|
|
if ($html) {
|
|
$header = '<html><table style="min-width: 450px;" class="block wide">';
|
|
$footer = '</table></html>';
|
|
}
|
|
|
|
my $rowOpen = "";
|
|
my $rowOpenEven = "";
|
|
my $rowOpenOdd = "";
|
|
my $colOpen = "";
|
|
my $txtOpen = "";
|
|
my $txtClose = "";
|
|
my $colClose = "\t\t\t";
|
|
my $rowClose = "";
|
|
|
|
if ($html) {
|
|
$rowOpen = '<tr>';
|
|
$rowOpenEven = '<tr class="even">';
|
|
$rowOpenOdd = '<tr class="odd">';
|
|
$colOpen = '<td>';
|
|
$txtOpen = "<b>";
|
|
$txtClose = "</b>";
|
|
$colClose = '</td>';
|
|
$rowClose = '</tr>';
|
|
}
|
|
|
|
push @ret,
|
|
$rowOpen
|
|
. $colOpen
|
|
. $txtOpen
|
|
. 'Package Name'
|
|
. $txtClose
|
|
. $colClose
|
|
. $colOpen
|
|
. $txtOpen
|
|
. 'Current Version'
|
|
. $txtClose
|
|
. $colClose
|
|
. $colOpen
|
|
. $txtOpen
|
|
. 'New Version'
|
|
. $txtClose
|
|
. $colClose
|
|
. $rowClose;
|
|
|
|
if ( ref($packages) eq "HASH" ) {
|
|
|
|
my $linecount = 1;
|
|
foreach my $package ( sort keys( %{$packages} ) ) {
|
|
next if ( $package eq "undefined" );
|
|
|
|
my $l = $linecount % 2 == 0 ? $rowOpenEven : $rowOpenOdd;
|
|
$l .= $colOpen . $package . $colClose;
|
|
$l .= $colOpen
|
|
. (
|
|
defined( $packages->{$package}{current} )
|
|
? $packages->{$package}{current}
|
|
: '?'
|
|
) . $colClose;
|
|
$l .= $colOpen
|
|
. (
|
|
defined( $packages->{$package}{latest} )
|
|
? $packages->{$package}{latest}
|
|
: '?'
|
|
) . $colClose;
|
|
$l .= $rowClose;
|
|
|
|
push @ret, $l;
|
|
$linecount++;
|
|
}
|
|
}
|
|
|
|
return $header . join( "\n", @ret ) . $footer;
|
|
}
|
|
|
|
#### my little helper
|
|
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;
|
|
}
|
|
|
|
1;
|
|
|
|
=pod
|
|
=encoding utf8
|
|
=item device
|
|
=item summary Module to control Node.js package installation and update
|
|
=item summary_DE Modul zur Bedienung der Node.js Paket Installation und Updates
|
|
|
|
=begin html
|
|
|
|
<a name="npmjs" id="npmjs"></a>
|
|
<h3>
|
|
Node.js installation and update
|
|
</h3>
|
|
<ul>
|
|
<u><b>npmjs - controls Node.js installation and updates</b></u><br>
|
|
This module allows to install, uninstall and update outdated Node.js packages using NPM package manager.<br>
|
|
Global installations will be controlled by default and running update/install/uninstall require sudo permissions like this:<br>
|
|
<br>
|
|
<code>
|
|
fhem ALL=NOPASSWD: /usr/bin/npm update *<br>
|
|
fhem ALL=NOPASSWD: /usr/bin/npm install *<br>
|
|
fhem ALL=NOPASSWD: /usr/bin/npm uninstall *
|
|
</code><br>
|
|
<br>
|
|
This line may easily be added to a new file in /etc/sudoers.d/fhem and will automatically included to /etc/sudoers from there.<br>
|
|
More restricted sudo settings are currently not supported.<br>
|
|
Only checking for outdated packages does not require any privileged access at all!<br>
|
|
<br>
|
|
<br>
|
|
<a name="npmjsdefine" id="npmjsdefine"></a><b>Define</b><br>
|
|
<ul>
|
|
<code>define <name> npmjs [<[user@]HOSTNAME[:port]>]</code><br>
|
|
<br>
|
|
Example:<br>
|
|
<ul>
|
|
<code>define fhemServerNpm npmjs</code><br>
|
|
</ul><br>
|
|
This command creates an npmjs instance named 'fhemServerNpm' to run commands on the local host machine.
|
|
Afterwards all information about installation and update state will be fetched. This will take a moment.<br>
|
|
<br>
|
|
If you would like to connect to a remote host, set the optional HOSTNAME to something different than 'localhost'.
|
|
In that case, make sure that SSH keys between the FHEM running user and remote server are setup appropriately.
|
|
</ul><br>
|
|
<br>
|
|
<a name="npmjsreadings" id="npmjsreadings"></a><b>Readings</b>
|
|
<ul>
|
|
<li>state - update status about the server
|
|
</li>
|
|
<li>nodejsVersion - installed Node.js version
|
|
</li>
|
|
<li>outdated - status about last update status sync
|
|
</li>
|
|
<li>updated - status about last update command
|
|
</li>
|
|
<li>installed - status about last install command
|
|
</li>
|
|
<li>uninstalled - status about last uninstall command
|
|
</li>
|
|
<li>updatesAvailable - number of available updates
|
|
</li>
|
|
</ul><br>
|
|
<br>
|
|
<a name="npmjsset" id="npmjsset"></a><b>Set</b>
|
|
<ul>
|
|
<li>outdated - fetch information about update state
|
|
</li>
|
|
<li>update - trigger complete or selected update process. this will take a moment
|
|
</li>
|
|
<li>install - Install one or more NPM packages. If Node.js is not installed on the server, it will offer to
|
|
initially install Node.js (APT compatible Linux distributions only). You may still install Node.js and
|
|
NPM manually and trigger to re-detect the installation by using any of the provided options. Existing
|
|
Node.js installations will never be overwritten and it will not be possible to upgrade Node.js using
|
|
this FHEM module!
|
|
</li>
|
|
<li>uninstall - Uninstall one or more NPM packages
|
|
</li>
|
|
</ul><br>
|
|
<br>
|
|
<a name="npmjsget" id="npmjsget"></a><b>Get</b>
|
|
<ul>
|
|
<li>showOutdatedList - list about available updates
|
|
</li>
|
|
<li>showErrorList - list errors that occured for the last command
|
|
</li>
|
|
</ul><br>
|
|
<br>
|
|
<a name="npmjsattribut" id="npmjsattribut"></a><b>Attributes</b>
|
|
<ul>
|
|
<li>disable - disables the device
|
|
</li>
|
|
<li>updateListReading - add Update List Reading as JSON
|
|
</li>
|
|
<li>npmglobal - work on global or user installation. Defaults to 1=global
|
|
</li>
|
|
<li>disabledForIntervals - disable device for interval time (13:00-18:30 or 13:00-18:30 22:00-23:00)
|
|
</li>
|
|
</ul>
|
|
</ul>
|
|
|
|
=end html
|
|
|
|
=begin html_DE
|
|
|
|
<a name="npmjs" id="npmjs"></a>
|
|
<h3>
|
|
Node.js Installation und Update
|
|
</h3>
|
|
<ul>
|
|
<u><b>npmjs - Bedienung der Node.js Installation und Updates</b></u><br>
|
|
Das Modul erlaubt es Node.js Pakete über den NPM Paket Manager zu installieren, zu deinstallieren und zu aktualisieren.<br>
|
|
Standardmäßig werden globale Installationen bedient und das Ausführen von update/install/uninstall erfordert sudo Berechtigungen wie diese:<br>
|
|
<br>
|
|
<code>fhem ALL=NOPASSWD: ALL</code><br>
|
|
<br>
|
|
Diese Zeile kann einfach in einer neuen Datei unter /etc/sudoers.d/fhem hinzugefügt werden und wird von dort automatisch in /etc/sudoers inkludiert.<br>
|
|
Restriktiviere sudo Einstellungen sind derzeit nicht möglich.<br>
|
|
Um nur die zu aktualisierenden Pakete zu überprüfen wird überhaupt kein priviligierter Zugriff benötigt!<br>
|
|
<br>
|
|
<br>
|
|
<a name="npmjsdefine" id="npmjsdefine"></a><b>Define</b><br>
|
|
<ul>
|
|
<code>define <name> npmjs [<[user@]HOSTNAME[:port]>]</code><br>
|
|
<br>
|
|
Beispiel:<br>
|
|
<ul>
|
|
<code>define fhemServer npmjs</code><br>
|
|
</ul><br>
|
|
Der Befehl erstellt eine npmjs Instanz mit dem Namen 'fhemServerNpm', um Kommandos auf der lokalen Host Maschine auszuführen.
|
|
Anschließend werden die alle Informationen über den Installations- und Update Status geholt. Dies kann einen Moment dauern.<br>
|
|
<br>
|
|
Wenn man sich zu einem entfernten Rechner verbinden möchte, kann man den optionalen HOSTNAME Parameter zu etwas anderem als 'localhost' setzen.
|
|
In diesem Fall muss auch sichergestellt sein, dass die SSH Schlüssel zwischen dem laufenden FHEM Benutzer und dem entfernten Server entsprechend konfiguriert sind.
|
|
</ul><br>
|
|
<br>
|
|
<a name="npmjsreadings" id="npmjsreadings"></a><b>Readings</b>
|
|
<ul>
|
|
<li>state - update Status des Servers
|
|
</li>
|
|
<li>nodejsVersion - installierte Node.js Version
|
|
</li>
|
|
<li>outdated - Status des letzten Update sync.
|
|
</li>
|
|
<li>updated - Status des letzten update Befehles
|
|
</li>
|
|
<li>installed - Status des letzten install Befehles
|
|
</li>
|
|
<li>uninstalled - Status des letzten uninstall Befehles
|
|
</li>
|
|
<li>updatesAvailable - Anzahl der verfügbaren Paketupdates
|
|
</li>
|
|
</ul><br>
|
|
<br>
|
|
<a name="npmjsset" id="npmjsset"></a><b>Set</b>
|
|
<ul>
|
|
<li>outdated - Holt aktuelle Informationen über den Updatestatus
|
|
</li>
|
|
<li>update - Führt ein komplettes oder selektives Update aus
|
|
</li>
|
|
<li>install - installiert ein oder mehrere NPM Pakete
|
|
</li>
|
|
<li>install - Installiert ein oder mehrere NPM Pakete. Wenn Node.js nicht installiert ist, wird die erstmalige
|
|
Installation von Node.js angeboten (nur für APT kompatible Linux Distributionen). Node.js kann weiterhin
|
|
manuell installiert werden. Über jede der angebotenen Optionen kann eine erneute Prüfung veranlasst
|
|
werden. Bestehende Node.js Installationen werden niemals überschrieben und es ist nicht möglich ein
|
|
Upgrade von Node.js über dieses FHEM Modul durchzuführen!
|
|
</li>
|
|
<li>uninstall - deinstalliert ein oder mehrere NPM Pakete
|
|
</li>
|
|
</ul><br>
|
|
<br>
|
|
<a name="npmjsget" id="npmjsget"></a><b>Get</b>
|
|
<ul>
|
|
<li>showOutdatedList - Paketiste aller zur Verfügung stehender Updates
|
|
</li>
|
|
<li>showErrorList - Liste aller aufgetretenden Fehler für das letzte Kommando
|
|
</li>
|
|
</ul><br>
|
|
<br>
|
|
<a name="npmjsattribut" id="npmjsattribut"></a><b>Attributes</b>
|
|
<ul>
|
|
<li>disable - Deaktiviert das Device
|
|
</li>
|
|
<li>updateListReading - fügt die Update Liste als ein zusäiches Reading im JSON Format ein.
|
|
</li>
|
|
<li>npmglobal - wechselt zwischen Global- und Benutzer-Installation. Standard ist 1=global
|
|
</li>
|
|
<li>disabledForIntervals - Deaktiviert das Device für eine bestimmte Zeit (13:00-18:30 or 13:00-18:30 22:00-23:00)
|
|
</li>
|
|
</ul>
|
|
</ul>
|
|
|
|
=end html_DE
|
|
|
|
=for :application/json;q=META.json 42_npmjs.pm
|
|
{
|
|
"abstract": "Module to control Node.js package installation and update",
|
|
"x_lang": {
|
|
"de": {
|
|
"abstract": "Modul zur Bedienung der Node.js Installation und Updates"
|
|
}
|
|
},
|
|
"keywords": [
|
|
"fhem-core",
|
|
"fhem-mod",
|
|
"fhem-mod-device",
|
|
"nodejs",
|
|
"node",
|
|
"npm"
|
|
],
|
|
"version": "v0.10.6",
|
|
"release_status": "stable",
|
|
"author": [
|
|
"Julian Pawlowski <julian.pawlowski@gmail.com>"
|
|
],
|
|
"x_fhem_maintainer": [
|
|
"loredo"
|
|
],
|
|
"x_fhem_maintainer_github": [
|
|
"jpawlowski"
|
|
],
|
|
"prereqs": {
|
|
"runtime": {
|
|
"requires": {
|
|
"FHEM": ">= 5.9.18623",
|
|
"perl": 5.014,
|
|
"GPUtils qw(GP_Import)": 0,
|
|
"JSON": 0,
|
|
"Data::Dumper": 0
|
|
},
|
|
"recommends": {
|
|
},
|
|
"suggests": {
|
|
}
|
|
}
|
|
},
|
|
"x_prereqs_os": {
|
|
"runtime": {
|
|
"requires": {
|
|
},
|
|
"recommends": {
|
|
"debian|ubuntu": 0
|
|
},
|
|
"suggests": {
|
|
}
|
|
}
|
|
},
|
|
"x_prereqs_os_debian": {
|
|
"runtime": {
|
|
"requires": {
|
|
},
|
|
"recommends": {
|
|
"openssh-client": 0
|
|
},
|
|
"suggests": {
|
|
}
|
|
}
|
|
},
|
|
"x_prereqs_os_ubuntu": {
|
|
"runtime": {
|
|
"requires": {
|
|
},
|
|
"recommends": {
|
|
"openssh-client": 0
|
|
},
|
|
"suggests": {
|
|
}
|
|
}
|
|
},
|
|
"x_prereqs_nodejs": {
|
|
"runtime": {
|
|
"requires": {
|
|
"node": 8.0,
|
|
"npm": 0
|
|
},
|
|
"recommends": {
|
|
},
|
|
"suggests": {
|
|
}
|
|
}
|
|
},
|
|
"x_prereqs_python": {
|
|
"runtime": {
|
|
"requires": {
|
|
},
|
|
"recommends": {
|
|
},
|
|
"suggests": {
|
|
}
|
|
}
|
|
},
|
|
"x_prereqs_binary_exec": {
|
|
"runtime": {
|
|
"requires": {
|
|
"/usr/bin/node|/usr/local/bin/node": 0,
|
|
"/usr/bin/npm|/usr/local/bin/npm": 0
|
|
},
|
|
"recommends": {
|
|
},
|
|
"suggests": {
|
|
"/usr/bin/ssh|/usr/local/bin/ssh": 0
|
|
}
|
|
}
|
|
},
|
|
"x_prereqs_sudo": {
|
|
"runtime": {
|
|
"requires": {
|
|
},
|
|
"recommends": {
|
|
"ALL=NOPASSWD: /usr/bin/npm update *": 0,
|
|
"ALL=NOPASSWD: /usr/local/bin/npm update *": 0
|
|
},
|
|
"suggests": {
|
|
"ALL=NOPASSWD: /usr/bin/npm install *": 0,
|
|
"ALL=NOPASSWD: /usr/local/bin/npm install *": 0,
|
|
"ALL=NOPASSWD: /usr/bin/npm uninstall *": 0,
|
|
"ALL=NOPASSWD: /usr/local/bin/npm uninstall *": 0
|
|
}
|
|
}
|
|
},
|
|
"x_prereqs_permissions_fileown": {
|
|
"runtime": {
|
|
"requires": {
|
|
},
|
|
"recommends": {
|
|
},
|
|
"suggests": {
|
|
}
|
|
}
|
|
},
|
|
"x_prereqs_permissions_filemod": {
|
|
"runtime": {
|
|
"requires": {
|
|
},
|
|
"recommends": {
|
|
},
|
|
"suggests": {
|
|
}
|
|
}
|
|
},
|
|
"resources": {
|
|
"license": [
|
|
"https://fhem.de/#License"
|
|
],
|
|
"homepage": "https://fhem.de/",
|
|
"bugtracker": {
|
|
"web": "https://forum.fhem.de/index.php/board,29.0.html",
|
|
"x_web_title": "Sonstige Systeme"
|
|
},
|
|
"repository": {
|
|
"type": "svn",
|
|
"url": "https://svn.fhem.de/fhem/",
|
|
"x_branch_master": "trunk",
|
|
"x_branch_dev": "trunk",
|
|
"web": "https://svn.fhem.de/"
|
|
}
|
|
}
|
|
}
|
|
=end :application/json;q=META.json
|
|
|
|
=cut
|