#!/usr/bin/perl ############################################################################## # $Id$ ############################################################################## # # presenced # checks for one or multiple bluetooth devices for their presence state # and report this to the 73_PRESENCE.pm module. # # Copyright by Markus Bloch # e-mail: Notausstieg0309@googlemail.com # # This file is part of fhem. # # Fhem 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. # # Fhem is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with fhem. If not, see . # ############################################################################## use IO::Socket; use IO::Select; use File::Basename; use Getopt::Long; use threads; use threads::shared; use Thread::Queue; use Time::HiRes qw(gettimeofday); use warnings; use strict; sub Log($$); sub timestamp(); sub daemonize(); sub doQuery($$); my $new_client; my $server; my $client; my $buf; my $querylocker :shared = int(time() - 15); my %queues; my $log_queue = Thread::Queue->new(); my $opt_d; my $opt_h; my $opt_v = 0; my $opt_p = 5111; my $opt_P = "/var/run/".basename($0).".pid"; my $opt_l; my $opt_n; Getopt::Long::Configure('bundling'); GetOptions( "d" => \$opt_d, "daemon" => \$opt_d, "n" => \$opt_n, "no-timestamps" => \$opt_n, "v+" => \$opt_v, "verbose+" => \$opt_v, "l=s" => \$opt_l, "logfile=s" => \$opt_l, "p=i" => \$opt_p, "port=i" => \$opt_p, "P=s" => \$opt_P, "pid-file=s" => \$opt_P, "h" => \$opt_h, "help" => \$opt_h ); Log 0, "=================================================" if($opt_l); Log 1, "started with PID $$"; if(-e "$opt_P") { print timestamp()."another process already running (PID file found at $opt_P)\n"; print timestamp()."aborted...\n"; exit 1; } sub print_usage () { print "Usage:\n"; print " presenced -d [-p ] [-P ] \n"; print " presenced [-h | --help]\n"; print "\n\nOptions:\n"; print " -p, --port\n"; print " TCP Port which should be used (Default: 5111)\n"; print " -P, --pid-file\n"; print " PID file for storing the local process id (Default: /var/run/".basename($0).".pid)\n"; print " -d, --daemon\n"; print " detach from terminal and run as background daemon\n"; print " -n, --no-timestamps\n"; print " do not output timestamps in log messages\n"; print " -v, --verbose\n"; print " Print detailed log output\n"; print " -h, --help\n"; print " Print detailed help screen\n"; } if($opt_d) { daemonize(); } if($opt_h) { print_usage(); exit; } open(PIDFILE, ">$opt_P") or die("Could not open PID file $opt_P: $!"); print PIDFILE $$."\n"; close PIDFILE; $server = new IO::Socket::INET ( LocalPort => $opt_p, Proto => 'tcp', Listen => 5, Reuse => 1, Type => SOCK_STREAM, KeepAlive => 1, Blocking => 0 ) or die "error while creating socket: $!\n"; Log 0, "created socket on ".$server->sockhost().":".$server->sockport(); my $listener = IO::Select->new(); $listener->add($server); my @new_handles; my %child_handles; my %child_config; my $thread_counter = 0; my $address; my $name; my $timeout; my $write_handle; my $server_pid; my @threads; my $sig_received = undef; $SIG{HUP} = sub { $sig_received = "SIGHUP"; }; $SIG{INT} = sub { $sig_received = "SIGINT"; }; $SIG{TERM} = sub { $sig_received = "SIGTERM"; }; $SIG{KILL} = sub { $sig_received = "SIGKILL"; }; $SIG{QUIT} = sub { $sig_received = "SIGQUIT"; }; $SIG{ABRT} = sub { $sig_received = "SIGABRT"; }; $SIG{PIPE} = sub { $sig_received = "SIGPIPE"; }; $server_pid = $$ unless(defined($server_pid)); while(1) { if($log_queue->pending) { Log 2, $log_queue->dequeue; } if(@new_handles = $listener->can_read(1)) { foreach my $client (@new_handles) { if($client == $server) { $new_client = $server->accept(); setsockopt($new_client, SOL_SOCKET, SO_KEEPALIVE, 1); # activate keep-alive $listener->add($new_client); Log 1, "new connection from ".$new_client->peerhost().":".$new_client->peerport(); } else { $buf = ''; $buf = <$client>; if($buf) { $buf =~ s/(^\s*|\s*$)//g; if($buf =~ /^\s*(?:[0-9a-f]{2}:){5}[0-9a-f]{2}\s*\|\s*\d+\s*$/i) { $client->send("command accepted\n"); Log 2, "received new command from ".$client->peerhost().":".$client->peerport()." - $buf"; ($address, $timeout) = split("\\|", $buf); $address =~ s/\s*//g; $timeout =~ s/\s*//g; $write_handle = $client; if(defined($child_handles{$client})) { Log 2, "sending new command to thread ".$child_handles{$client}->tid()." for client ".$client->peerhost().":".$client->peerport(); $queues{$child_handles{$client}->tid()}->enqueue("new|".$address."|".$timeout); } else { $thread_counter++; $queues{$thread_counter} = Thread::Queue->new(); my $new_thread = threads->new(\&doQuery, ($write_handle, $address, $timeout)); Log 2, "created thread ".$new_thread->tid()." for processing device $address within $timeout seconds for peer ".$client->peerhost().":".$client->peerport(); $new_thread->detach(); $child_handles{$client} = $new_thread; } } elsif(lc($buf) =~ /^\s*now\s*$/) { Log 2, "received now command from client ".$client->peerhost().":".$client->peerport(); if(defined($child_handles{$client})) { Log 2, "signalling thread ".$child_handles{$client}->tid()." for an instant test for client ".$client->peerhost().":".$client->peerport(); $queues{$child_handles{$client}->tid()}->enqueue("now"); $client->send("command accepted\n"); } else { $client->send("no command running\n"); } } elsif(lc($buf) =~ /^\s*stop\s*$/) { Log 2, "received stop command from client ".$client->peerhost().":".$client->peerport(); if(defined($child_handles{$client})) { Log 2, "sending thread ".$child_handles{$client}->tid()." the stop command for client ".$client->peerhost().":".$client->peerport(); $queues{$child_handles{$client}->tid()}->enqueue("stop"); $client->send("command accepted\n"); delete($child_handles{$client}); } else { $client->send("no command running\n"); } } elsif(lc($buf) =~ /^\s*ping\s*$/) { Log 2, "received ping command from client ".$client->peerhost().":".$client->peerport(); $client->send("pong\n"); Log 1, "closed connection from ".$client->peerhost().":".$client->peerport(); $listener->remove($client); shutdown($client, 2); close $client; $client = undef; } else { $client->send("command rejected\n"); Log 1, "received invalid command >>$buf<< from client ".$client->peerhost().":".$client->peerport(); } } else { Log 1, "closed connection from ".$client->peerhost().":".$client->peerport(); $listener->remove($client); if(defined($child_handles{$client})) { Log 2, "killing thread ".$child_handles{$client}->tid()." for client ".$client->peerhost(); $queues{$child_handles{$client}->tid()}->enqueue("stop"); delete($child_handles{$client}); } shutdown($client, 2); close $client; $client = undef; Log 1, "closed successfully all threads"; } } } } if(defined($sig_received)) { Log 0, "caught $sig_received"; unlink($opt_P); Log 1, "removed PID-File $opt_P"; Log 0, "exiting"; exit; } } sub daemonize() { use POSIX; POSIX::setsid or die "setsid $!"; my $pid = fork(); if($pid < 0) { die "fork: $!"; } elsif($pid) { Log 0, "forked with PID $pid"; exit 0; } chdir "/"; umask 0; foreach (0 .. (POSIX::sysconf (&POSIX::_SC_OPEN_MAX) || 1024)) { POSIX::close $_ } open (STDIN, "/dev/null"); open (STDERR, ">&STDOUT"); } sub doQuery($$) { my ($write_handle, $address, $timeout) = @_; my $return; my $hcitool; my $nextrun = gettimeofday(); my $cmd; my $run = 1; if($address and $timeout) { THREADLOOP: while($run) { if(exists($queues{threads->tid()}) and $queues{threads->tid()}->pending) { $cmd = $queues{threads->tid()}->dequeue; Log 2, threads->tid()."|received command: $cmd"; if($cmd eq "now") { $log_queue->enqueue(threads->tid()."|performing an instant test"); $nextrun = gettimeofday(); } elsif($cmd eq "stop") { $log_queue->enqueue(threads->tid()."|shutting down thread"); $run = 0; last THREADLOOP; } elsif($cmd =~ /^new\|/) { ($cmd, $address, $timeout) = split("\\|", $cmd); $nextrun = gettimeofday(); $log_queue->enqueue(threads->tid()."|new address: $address - new timeout $timeout"); } } if($write_handle) { if($nextrun <= gettimeofday()) { { lock($querylocker); if($querylocker gt (gettimeofday() - 2)) { $log_queue->enqueue(threads->tid()."|waiting before hcitool command execution because last command was executed within the last 2 seconds"); sleep rand(1) + 1; } $hcitool = qx(which hcitool); chomp $hcitool; if( -x "$hcitool") { $return = qx(hcitool name $address 2>/dev/null); } else { $write_handle->send("error\n") if(defined($write_handle)); } $querylocker = gettimeofday(); } chomp $return; if(not $return =~ /^\s*$/) { $write_handle->send("present;$return\n") if(defined($write_handle)); } else { $write_handle->send("absence\n") if(defined($write_handle)); } $nextrun = gettimeofday() + $timeout; } } sleep 1; } } delete($queues{threads->tid()}) if(exists($queues{threads->tid()})); } sub timestamp() { return POSIX::strftime("%Y-%m-%d %H:%M:%S - ",localtime) unless($opt_n); return ""; } sub Log($$) { my ($loglevel, $message) = @_; my $thread = 0; if($message =~ /^\d+\|/) { ($thread, $message) = split("\\|", $message); } if($loglevel <= $opt_v) { if($opt_l) { open(LOGFILE, ">>$opt_l") or die ("could not open logfile: $opt_l"); } else { open (LOGFILE, ">&STDOUT") or die("cannot open STDOUT"); } print LOGFILE ($opt_l?"":"\r").timestamp().($opt_v >= 2 ? ($thread > 0 ? "(Thread $thread)" : "(Main Thread)")." - ":"").$message."\n"; close(LOGFILE); } }