From 94acaa96dc1df1d410a46b81cec278dabc68954b Mon Sep 17 00:00:00 2001 From: rudolfkoenig <> Date: Sun, 25 Aug 2013 11:49:30 +0000 Subject: [PATCH] Install FHEM as a Windows service (by T.E.) git-svn-id: https://svn.fhem.de/fhem/trunk/fhem@3788 2b470e98-0d58-463d-a4d8-8e2adae1ed80 --- CHANGED | 14 +- FHEM/WinService.pm | 245 ++++++++++++++++++ Makefile | 3 +- docs/{HOWTO_WinTest.txt => HOWTO_Windows.txt} | 17 +- fhem.pl | 57 ++-- 5 files changed, 310 insertions(+), 26 deletions(-) create mode 100755 FHEM/WinService.pm rename docs/{HOWTO_WinTest.txt => HOWTO_Windows.txt} (71%) diff --git a/CHANGED b/CHANGED index e7f0bf9bb..2c7f7a538 100644 --- a/CHANGED +++ b/CHANGED @@ -1,11 +1,13 @@ -# Add changes at the top of the list. Keep it in ASCII +# Add changes at the top of the list. Keep it in ASCII, and 80-char wide. - SVN - - feature: new module 33_readingsGroup to display a collection of readings from - on or more devices. this will replace weblink readings. (by justme1968) + - feature: install FHEM as Windows service by T.E., see docs/HOWTO_Windows.txt + - feature: new module 33_readingsGroup to display a collection of readings + from one or more devices. this will replace weblink readings. + (by justme1968) - feature: setreading command added - - change: DbLog: by using DbLog a new Attribute DbLogExclude will be propagated - to all Devices. DbLogExclue will work as regexp to exclude - defined readings to log + - change: DbLog: by using DbLog a new Attribute DbLogExclude will be + propagated to all Devices. DbLogExclue will work as regexp to + exclude defined readings to log - change: loglevel attribute deprecated/replaced by the verbose attribute - change: VIERA: changed several readings/commands according to DevelopmentGuidelinesAV. See FHEM Wiki and commandref for more diff --git a/FHEM/WinService.pm b/FHEM/WinService.pm new file mode 100755 index 000000000..c1cc2f188 --- /dev/null +++ b/FHEM/WinService.pm @@ -0,0 +1,245 @@ +################################################################ +# +# Copyright notice +# +# (c) 2013 Thomas Eckardt (Thomas.Eckardt@thockar.com) +# +# This script is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# The GNU General Public License can be found at +# http://www.gnu.org/copyleft/gpl.html. +# A copy is found in the textfile GPL.txt and important notices to the license +# from the author is found in LICENSE.txt distributed with these scripts. +# +# This script is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# This copyright notice MUST APPEAR in all copies of the script! +# +################################################################ + +# $Id: WinService.pm,v 1.01 2013-08-22 18:15:00Z TE $ + +package FHEM::WinService; + +use strict; + +sub __installService($$$); +sub __initService($$); +sub new($$$); + +use vars qw($VERSION); + +$VERSION = $1 if('$Id: WinService.pm,v 1.01 2013-08-22 18:15:00Z TE $' =~ /,v ([\d.]+) /); + + +################################################### +# Windows Service Handler +# +# install/remove or possibly start fhem as a Windows Service + +sub +new ($$$) { + my ($class, $argv) = @_; + + my $fhem = $0; + $fhem =~ s/\\/\//go; + my $fhembase = $fhem; + $fhembase =~ s/\/?[^\/]+$//o; + if (! $fhembase && eval('use Cwd();1;')) { + $fhembase = Cwd::cwd(); + $fhembase =~ s/\\/\//go; + } + my $larg = $argv->[@$argv-1]; + + if ($larg eq '-i') { + if (! $fhembase) { + print "error: unable to detect fhem folder - cancel\n"; + exit 0; + } + $fhem = $fhembase.'/'.$fhem if $fhem !~ /\//o; + my $cfg = $argv->[0]; + $cfg =~ s/\\/\//go; + $cfg = $fhembase.'/'.$cfg if $cfg !~ /\//o; + print "try to install fhem windows service as: $^X $fhem $cfg\n"; + __installService('-i' , $fhem, $cfg); + exit 0; + } elsif ($larg eq '-u') { + print "try to remove fhem windows service\n"; + __installService('-u',undef,undef); + exit 0; + } else { + $class = ref $class || $class; + bless my $self = {}, $class; + $self->{ServiceLog} = []; + @{$self->{ServiceLog}} = __initService($self, $argv->[0]); + return $self; + } +} +################################################### + + +################################################### +# from here are internal subs only ! +################################################### + + +################################################### +# install or remove fhem as a Windows Service +# + +sub +__installService($$$) +{ + eval(<<'EOT') or print "error: $@\n)"; + use Win32::Daemon; + my $p; + my $p2; + + if(lc $_[0] eq '-u') { + system('cmd.exe /C net stop fhem'); + sleep(1); + Win32::Daemon::DeleteService('','fhem') || + print "Failed to remove fhem service: " . + Win32::FormatMessage( Win32::Daemon::GetLastError() ) . "\n" & return; + print "Successfully removed service fhem\n"; + } elsif( lc $_[0] eq '-i') { + unless($p=$_[1]) { + $p=$0; + $p=~s/\w+\.pl/fhem.pl/o; + } + if($p2=$_[2]) { + $p2=~s/[\\\/]$//o; + } else { + $p2=$p; $p2=~s/\.pl/.cfg/io; + } + my %Hash = ( + name => 'fhem', + display => 'fhem server', + path => "\"$^X\"", + user => '', + pwd => '', + parameters => "\"$p\" \"$p2\"", + ); + if( Win32::Daemon::CreateService( \%Hash ) ) { + print "fhem service successfully added.\n"; + } else { + print "Failed to add fhem service: " . + Win32::FormatMessage( Win32::Daemon::GetLastError() ) . "\n"; + print "Note: if you're getting an error: Service is marked for ". + "deletion, then close the service control manager window". + " and try again.\n"; + } + } + 1; +EOT +} + + +################################################### +# check if called from SCM and start the service if so +# + +sub +__initService ($$) { + my ($self, $arg) = @_; + my @ServiceLog; + + # check how we are called from the OS - Console or SCM + + # Win32 Daemon and Console module installed ? + if( eval("use Win32::Daemon; use Win32::Console; 1;") ) { + eval(<<'EOT'); + my $cmdlin = Win32::Console::_GetConsoleTitle () ? 1 : 0; + + eval{&main::doGlobalDef($arg);}; # we need some config here + + if ($cmdlin) { + $self->{AsAService} = 0; + } else { + $self->{AsAService} = 1; + $self->{ServiceStopping} = 0; + $main::attr{global}{nofork}=1; # this has to be set here + push @ServiceLog, 'registering fhem as Windows Service'; + Win32::Daemon::StartService(); + + # Wait until the service manager is ready for us to continue... + my $i = 0; + while( SERVICE_START_PENDING != Win32::Daemon::State() && $i < 60) { + # looping indefinitely and waiting to start + sleep( 1 ); + $i++; + } + if ($i > 59) { + push @ServiceLog,'unable to register fhem in SCM - cancel'; + die "unable to register fhem in SCM - cancel\n"; + } + Win32::Daemon::State( SERVICE_RUNNING ); + push @ServiceLog,'starting fhem as a service'; + + # this sub is called in the main loop to check the service state + $self->{serviceCheck} = sub { + return unless $self->{AsAService}; + my $state = Win32::Daemon::State(); + my %idlestate = ( + Win32::Daemon::SERVICE_PAUSE_PENDING => 1, + Win32::Daemon::SERVICE_CONTINUE_PENDING => -1 + ); + if( $state == SERVICE_STOP_PENDING ) { + if ($self->{ServiceStopping} == 0) { + $self->{ServiceStopping} = 1; + &main::Log(1,'service stopping'); + #ask SCM for a grace time (30 seconds) to shutdown + Win32::Daemon::State( SERVICE_STOP_PENDING, 30000 ); + &main::Log(1, 'service stopped'); + &main::CommandShutdown(undef, undef); + $self->{ServiceStopping} = 2; + Win32::Daemon::State( SERVICE_STOPPED ); + Win32::Daemon::StopService(); + # be nice, tell we stopped + exit 0; + } elsif ($self->{ServiceStopping} == 1) { + # keep telling SCM we're stopping and didn't hang + Win32::Daemon::State( SERVICE_STOP_PENDING, 30000 ); + } + } elsif ( $state == SERVICE_PAUSE_PENDING ) { + Win32::Daemon::State( SERVICE_PAUSED ); + $self->{allIdle} = $idlestate{$state}; + &main::Log(1,'pausing service'); + } elsif ( $state == SERVICE_CONTINUE_PENDING ) { + Win32::Daemon::State( SERVICE_RUNNING ); + $self->{allIdle} = $idlestate{$state}; + &main::Log(1, 'continue service'); + } else { + my $PrevState = SERVICE_RUNNING; + $PrevState = SERVICE_STOPPED if $self->{ServiceStopping}; + $PrevState = SERVICE_PAUSED if $self->{allIdle} > 0; + Win32::Daemon::State( $PrevState ); + undef $self->{allIdle} + if ($PrevState == SERVICE_RUNNING); + } + }; + } # end if ($cmdlin) +EOT + if ($@) { # we got some Perl errors in eval + push @ServiceLog, "error: $@"; + $self->{serviceCheck} = sub {}; # set it - could be destroyed + $self->{AsAService} = 0; + } + push @ServiceLog,'starting in console mode' + unless $self->{AsAService}; + } else { + $self->{AsAService} = 0; + push @ServiceLog,'starting in console mode'; + } + return @ServiceLog; +} +################################################### + +1; + diff --git a/Makefile b/Makefile index d02bacdb6..63c406da5 100644 --- a/Makefile +++ b/Makefile @@ -91,11 +91,12 @@ dist: cp -r fhem.pl fhem.cfg CHANGED HISTORY Makefile README.SVN\ FHEM contrib docs www webfrontend .f mkdir .f/log + touch .f/log/empty_file.txt (cd .f; perl contrib/commandref_join.pl) find .f -name .svn -print | xargs rm -rf find .f -name \*.orig -print | xargs rm -f find .f -name .#\* -print | xargs rm -f - find .f -type f -print | grep -v Makefile |\ + find .f -type f -print | grep -v Makefile | grep -v SWAP |\ xargs perl -pi -e 's/=VERS=/$(VERS)/g;s/=DATE=/$(DATE)/g' mv .f $(DESTDIR) tar cf - $(DESTDIR) | gzip > $(DESTDIR).tar.gz diff --git a/docs/HOWTO_WinTest.txt b/docs/HOWTO_Windows.txt similarity index 71% rename from docs/HOWTO_WinTest.txt rename to docs/HOWTO_Windows.txt index 24ca1314a..fa4009da7 100644 --- a/docs/HOWTO_WinTest.txt +++ b/docs/HOWTO_Windows.txt @@ -1,3 +1,8 @@ +The following description will show you how to install FHEM on Windows on a separate +USB-Drive, without any Windows-registry modifications. +You can use the internal HD for installation too, and you can register fhem as +a service, see below. + Install FHEM: Download the latest fhem-X.Y.tar.gz package from http://fhem.de#Download (currently it is fhem-5.4.tar.gz), and unpack it into a directory where you @@ -27,7 +32,7 @@ Start FHEM: Connect to the FHEM Web frontend (FHEMWEB): - Start your browser (Firefox,Chrome or Safari are preferred) and open + Start your browser (Firefox, Chrome or Safari are preferred) and open http://localhost:8083/fhem You'll see a smiling-house icon on a light-yellow background. @@ -49,3 +54,13 @@ not mandatory): i.e. arrow up and RETURN or type in perl\bin\perl fhem.pl fhem.cfg again. + + +Install FHEM as a service (better to install perl on the internal hard-disk for +this scenario): + Terminate fhem by typing shutdown again in the FHEMWEB command line. + Install the missing Win32::Daemon perl module by typing in the command window: + F:\tmp\fhem-5.4> PATH=F:\tmp\fhem-5.4\c\bin;F:\tmp\fhem-5.4\perl\bin;%PATH% + F:\tmp\fhem-5.4> perl\bin\cpan -i Win32::Daemon + Install FHEM as a service + F:\tmp\fhem-5.4> perl\bin\perl fhem.pl fhem.cfg -i diff --git a/fhem.pl b/fhem.pl index 1e101b35f..0f4843faf 100755 --- a/fhem.pl +++ b/fhem.pl @@ -4,7 +4,7 @@ # # Copyright notice # -# (c) 2005-2012 +# (c) 2005-2013 # Copyright: Rudolf Koenig (r dot koenig at koeniglich dot de) # All rights reserved # @@ -24,8 +24,6 @@ # GNU General Public License for more details. # # This copyright notice MUST APPEAR in all copies of the script! -# Thanks for Tosti's site () -# for inspiration. # # Homepage: http://fhem.de # @@ -59,6 +57,7 @@ sub FmtDateTime($); sub FmtTime($); sub GetLogLevel(@); sub GetTimeSpec($); +sub GlobalAttr($$$$); sub HandleArchiving($); sub HandleTimeout(); sub IOWrite($@); @@ -174,6 +173,7 @@ use vars qw(%addNotifyCB); # Used by event enhancers (e.g. avarage) use vars qw(%inform); # Used by telnet_ActivateInform use vars qw($reread_active); +use vars qw($winService); # the Windows Service object my $AttrList = "verbose:0,1,2,3,4,5 room group comment alias ". "eventMap userReadings"; @@ -292,6 +292,10 @@ if(int(@ARGV) < 1) { print "Usage:\n"; print "as server: fhem configfile\n"; print "as client: fhem [host:]port cmd cmd cmd...\n"; + if($^O =~ m/Win/) { + print "install as windows service: fhem.pl configfile -i\n"; + print "uninstall the windows service: fhem.pl -u\n"; + } CommandHelp(undef, undef); exit(1); } @@ -326,7 +330,7 @@ if($^O !~ m/Win/ && $< == 0) { ################################################### # Client code -if(int(@ARGV) > 1) { +if(int(@ARGV) > 1 && $ARGV[$#ARGV] ne "-i") { my $buf; my $addr = shift @ARGV; $addr = "localhost:$addr" if($addr !~ m/:/); @@ -349,27 +353,30 @@ if(int(@ARGV) > 1) { ################################################### -# for debugging -sub -Debug($) { - my $msg= shift; - Log 1, "DEBUG>" . $msg; +# Windows Service Support: install/remove or start the fhem service +if($^O =~ m/Win/) { + (my $dir = $0) =~ s+[/\\][^/\\]*$++; # Find the FHEM directory + chdir($dir); + $winService = eval {require FHEM::WinService; FHEM::WinService->new(\@ARGV);}; + if((!$winService || $@) && ($ARGV[$#ARGV] eq "-i" || $ARGV[$#ARGV] eq "-u")) { + print "Cannot initialize FHEM::WinService: $@, exiting.\n"; + exit 0; + } } -################################################### - +$winService ||= {}; ################################################### # Server initialization doGlobalDef($ARGV[0]); # As newer Linux versions reset serial parameters after fork, we parse the -# config file after the fork. Since need some global attr parameters before, we +# config file after the fork. But we need some global attr parameters before, so we # read them here. setGlobalAttrBeforeFork($attr{global}{configfile}); +Log 1, $_ for eval{@{$winService->{ServiceLog}};}; + if($^O =~ m/Win/ && !$attr{global}{nofork}) { - Log 1, "Forcing 'attr global nofork' on WINDOWS"; - Log 1, "set it in the config file to avoid this message"; $attr{global}{nofork}=1; } @@ -457,8 +464,10 @@ while (1) { } $timeout = $readytimeout if(keys(%readyfnlist) && (!defined($timeout) || $timeout > $readytimeout)); + $timeout = 5 if $winService->{AsAService} && $timeout > 5; my $nfound = select($rout=$rin, undef, undef, $timeout); + $winService->{serviceCheck}->() if($winService->{serviceCheck}); CommandShutdown(undef, undef) if($sig_term); if($nfound < 0) { @@ -965,9 +974,10 @@ OpenLogfile($) close(LOG); $logopened=0; $currlogfile = $param; - if($currlogfile eq "-") { - open LOG, '>&STDOUT' or die "Can't dup stdout: $!"; + # STDOUT is closed in windows services per default + if(!$winService->{AsAService} && $currlogfile eq "-") { + open LOG, '>&STDOUT' || die "Can't dup stdout: $!"; } else { @@ -1213,7 +1223,12 @@ CommandShutdown($$) WriteStatefile(); unlink($attr{global}{pidfilename}) if($attr{global}{pidfilename}); if($param && $param eq "restart") { - system("(sleep 2; exec $^X $0 $attr{global}{configfile})&"); + if ($^O !~ m/Win/) { + system("(sleep 2; exec $^X $0 $attr{global}{configfile})&"); + } elsif ($winService->{AsAService}) { + # use the OS SCM to stop and start the service + exec('cmd.exe /C net stop fhem & net start fhem'); + } } exit(0); } @@ -1871,7 +1886,7 @@ getAllSets($) } sub -GlobalAttr($$) +GlobalAttr($$$$) { my ($type, $me, $name, $val) = @_; @@ -3533,4 +3548,10 @@ utf8ToLatin1($) return $s; } +sub +Debug($) { + my $msg= shift; + Log 1, "DEBUG>" . $msg; +} + 1;