presenced 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429
  1. #!/usr/bin/perl
  2. ##############################################################################
  3. # $Id: presenced 10988 2016-03-04 17:27:36Z markusbloch $
  4. ##############################################################################
  5. #
  6. # presenced
  7. # checks for one or multiple bluetooth devices for their presence state
  8. # and report this to the 73_PRESENCE.pm module.
  9. #
  10. # Copyright by Markus Bloch
  11. # e-mail: Notausstieg0309@googlemail.com
  12. #
  13. # This file is part of fhem.
  14. #
  15. # Fhem is free software: you can redistribute it and/or modify
  16. # it under the terms of the GNU General Public License as published by
  17. # the Free Software Foundation, either version 2 of the License, or
  18. # (at your option) any later version.
  19. #
  20. # Fhem is distributed in the hope that it will be useful,
  21. # but WITHOUT ANY WARRANTY; without even the implied warranty of
  22. # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  23. # GNU General Public License for more details.
  24. #
  25. # You should have received a copy of the GNU General Public License
  26. # along with fhem. If not, see <http://www.gnu.org/licenses/>.
  27. #
  28. ##############################################################################
  29. use IO::Socket;
  30. use IO::Select;
  31. use File::Basename;
  32. use Getopt::Long;
  33. use threads;
  34. use threads::shared;
  35. use Thread::Queue;
  36. use Time::HiRes qw(gettimeofday);
  37. use warnings;
  38. use strict;
  39. sub Log($$);
  40. sub timestamp();
  41. sub daemonize();
  42. sub doQuery($$);
  43. my $new_client;
  44. my $server;
  45. my $client;
  46. my $buf;
  47. my $querylocker :shared = int(time() - 15);
  48. my %queues;
  49. my $log_queue = Thread::Queue->new();
  50. my $opt_d;
  51. my $opt_h;
  52. my $opt_v = 0;
  53. my $opt_p = 5111;
  54. my $opt_P = "/var/run/".basename($0).".pid";
  55. my $opt_l;
  56. Getopt::Long::Configure('bundling');
  57. GetOptions(
  58. "d" => \$opt_d, "daemon" => \$opt_d,
  59. "v+" => \$opt_v, "verbose+" => \$opt_v,
  60. "l=s" => \$opt_l, "logfile=s" => \$opt_l,
  61. "p=i" => \$opt_p, "port=i" => \$opt_p,
  62. "P=s" => \$opt_P, "pid-file=s" => \$opt_P,
  63. "h" => \$opt_h, "help" => \$opt_h
  64. );
  65. Log 0, "=================================================" if($opt_l);
  66. Log 1, "started with PID $$";
  67. if(-e "$opt_P")
  68. {
  69. print timestamp()." another process already running (PID file found at $opt_P)\n";
  70. print timestamp()." aborted...\n";
  71. exit 1;
  72. }
  73. sub print_usage ()
  74. {
  75. print "Usage:\n";
  76. print " presenced -d [-p <port>] [-P <filename>] \n";
  77. print " presenced [-h | --help]\n";
  78. print "\n\nOptions:\n";
  79. print " -p, --port\n";
  80. print " TCP Port which should be used (Default: 5111)\n";
  81. print " -P, --pid-file\n";
  82. print " PID file for storing the local process id (Default: /var/run/".basename($0).".pid)\n";
  83. print " -d, --daemon\n";
  84. print " detach from terminal and run as background daemon\n";
  85. print " -v, --verbose\n";
  86. print " Print detailed log output\n";
  87. print " -h, --help\n";
  88. print " Print detailed help screen\n";
  89. }
  90. if($opt_d)
  91. {
  92. daemonize();
  93. }
  94. if($opt_h)
  95. {
  96. print_usage();
  97. exit;
  98. }
  99. open(PIDFILE, ">$opt_P") or die("Could not open PID file $opt_P: $!");
  100. print PIDFILE $$."\n";
  101. close PIDFILE;
  102. $server = new IO::Socket::INET (
  103. LocalPort => $opt_p,
  104. Proto => 'tcp',
  105. Listen => 5,
  106. Reuse => 1,
  107. Type => SOCK_STREAM,
  108. KeepAlive => 1,
  109. Blocking => 0
  110. ) or die "error while creating socket: $!\n";
  111. Log 0, "created socket on ".$server->sockhost().":".$server->sockport();
  112. my $listener = IO::Select->new();
  113. $listener->add($server);
  114. my @new_handles;
  115. my %child_handles;
  116. my %child_config;
  117. my $thread_counter = 0;
  118. my $address;
  119. my $name;
  120. my $timeout;
  121. my $write_handle;
  122. my $server_pid;
  123. my @threads;
  124. my $sig_received = undef;
  125. $SIG{HUP} = sub { $sig_received = "SIGHUP"; };
  126. $SIG{INT} = sub { $sig_received = "SIGINT"; };
  127. $SIG{TERM} = sub { $sig_received = "SIGTERM"; };
  128. $SIG{KILL} = sub { $sig_received = "SIGKILL"; };
  129. $SIG{QUIT} = sub { $sig_received = "SIGQUIT"; };
  130. $SIG{ABRT} = sub { $sig_received = "SIGABRT"; };
  131. $SIG{PIPE} = sub { $sig_received = "SIGPIPE"; };
  132. $server_pid = $$ unless(defined($server_pid));
  133. while(1)
  134. {
  135. if($log_queue->pending)
  136. {
  137. Log 2, $log_queue->dequeue;
  138. }
  139. if(@new_handles = $listener->can_read(1))
  140. {
  141. foreach my $client (@new_handles)
  142. {
  143. if($client == $server)
  144. {
  145. $new_client = $server->accept();
  146. $listener->add($new_client);
  147. Log 1, "new connection from ".$new_client->peerhost().":".$new_client->peerport();
  148. }
  149. else
  150. {
  151. $buf = '';
  152. $buf = <$client>;
  153. if($buf)
  154. {
  155. $buf =~ s/(^\s*|\s*$)//g;
  156. if($buf =~ /^\s*([0-9a-fA-F]{2}:){5}[0-9a-fA-F]{2}\s*\|\s*\d+\s*$/)
  157. {
  158. $client->send("command accepted\n");
  159. Log 2, "received new command from ".$client->peerhost().":".$client->peerport()." - $buf";
  160. ($address, $timeout) = split("\\|", $buf);
  161. $address =~ s/\s*//g;
  162. $timeout =~ s/\s*//g;
  163. $write_handle = $client;
  164. if(defined($child_handles{$client}))
  165. {
  166. Log 2, "sending new command to thread ".$child_handles{$client}->tid()." for client ".$client->peerhost().":".$client->peerport();
  167. $queues{$child_handles{$client}->tid()}->enqueue("new|".$address."|".$timeout);
  168. }
  169. else
  170. {
  171. $thread_counter++;
  172. $queues{$thread_counter} = Thread::Queue->new();
  173. my $new_thread = threads->new(\&doQuery, ($write_handle, $address, $timeout));
  174. Log 2, "created thread ".$new_thread->tid()." for processing device $address within $timeout seconds for peer ".$client->peerhost().":".$client->peerport();
  175. $new_thread->detach();
  176. $child_handles{$client} = $new_thread;
  177. }
  178. }
  179. elsif(lc($buf) =~ /^\s*now\s*$/)
  180. {
  181. Log 2, "received now command from client ".$client->peerhost().":".$client->peerport();
  182. if(defined($child_handles{$client}))
  183. {
  184. Log 2, "signalling thread ".$child_handles{$client}->tid()." for an instant test for client ".$client->peerhost().":".$client->peerport();
  185. $queues{$child_handles{$client}->tid()}->enqueue("now");
  186. $client->send("command accepted\n");
  187. }
  188. else
  189. {
  190. $client->send("no command running\n");
  191. }
  192. }
  193. elsif(lc($buf) =~ /^\s*stop\s*$/)
  194. {
  195. Log 2, "received stop command from client ".$client->peerhost().":".$client->peerport();
  196. if(defined($child_handles{$client}))
  197. {
  198. Log 2, "sending thread ".$child_handles{$client}->tid()." the stop command for client ".$client->peerhost().":".$client->peerport();
  199. $queues{$child_handles{$client}->tid()}->enqueue("stop");
  200. $client->send("command accepted\n");
  201. delete($child_handles{$client});
  202. }
  203. else
  204. {
  205. $client->send("no command running\n");
  206. }
  207. }
  208. else
  209. {
  210. $client->send("command rejected\n");
  211. Log 1, "received invalid command >>$buf<< from client ".$client->peerhost().":".$client->peerport();
  212. }
  213. }
  214. else
  215. {
  216. Log 1, "closed connection from ".$client->peerhost().":".$client->peerport();
  217. $listener->remove($client);
  218. if(defined($child_handles{$client}))
  219. {
  220. Log 2, "killing thread ".$child_handles{$client}->tid()." for client ".$client->peerhost();
  221. $queues{$child_handles{$client}->tid()}->enqueue("stop");
  222. delete($child_handles{$client});
  223. }
  224. shutdown($client, 2);
  225. close $client;
  226. $client = undef;
  227. Log 1, "closed successfully all threads";
  228. }
  229. }
  230. }
  231. }
  232. if(defined($sig_received))
  233. {
  234. Log 0, "caught $sig_received";
  235. unlink($opt_P);
  236. Log 1, "removed PID-File $opt_P";
  237. Log 0, "exiting";
  238. exit;
  239. }
  240. }
  241. sub daemonize()
  242. {
  243. use POSIX;
  244. POSIX::setsid or die "setsid $!";
  245. my $pid = fork();
  246. if($pid < 0)
  247. {
  248. die "fork: $!";
  249. }
  250. elsif($pid)
  251. {
  252. Log 0, "forked with PID $pid";
  253. exit 0;
  254. }
  255. chdir "/";
  256. umask 0;
  257. foreach (0 .. (POSIX::sysconf (&POSIX::_SC_OPEN_MAX) || 1024)) { POSIX::close $_ }
  258. open (STDIN, "</dev/null");
  259. open (STDOUT, ">/dev/null");
  260. open (STDERR, ">&STDOUT");
  261. }
  262. sub doQuery($$)
  263. {
  264. my ($write_handle, $address, $timeout) = @_;
  265. my $return;
  266. my $hcitool;
  267. my $nextrun = gettimeofday();
  268. my $cmd;
  269. my $run = 1;
  270. if($address and $timeout)
  271. {
  272. THREADLOOP: while($run)
  273. {
  274. if(exists($queues{threads->tid()}) and $queues{threads->tid()}->pending)
  275. {
  276. $cmd = $queues{threads->tid()}->dequeue;
  277. Log 2, threads->tid()."|received command: $cmd";
  278. if($cmd eq "now")
  279. {
  280. $log_queue->enqueue(threads->tid()."|performing an instant test");
  281. $nextrun = gettimeofday();
  282. }
  283. elsif($cmd eq "stop")
  284. {
  285. $log_queue->enqueue(threads->tid()."|shutting down thread");
  286. $run = 0;
  287. last THREADLOOP;
  288. }
  289. elsif($cmd =~ /^new\|/)
  290. {
  291. ($cmd, $address, $timeout) = split("\\|", $cmd);
  292. $nextrun = gettimeofday();
  293. $log_queue->enqueue(threads->tid()."|new address: $address - new timeout $timeout");
  294. }
  295. }
  296. if($write_handle)
  297. {
  298. if($nextrun <= gettimeofday())
  299. {
  300. {
  301. lock($querylocker);
  302. if($querylocker gt (gettimeofday() - 2))
  303. {
  304. $log_queue->enqueue(threads->tid()."|waiting before hcitool command execution because last command was executed within the last 2 seconds");
  305. sleep rand(1) + 1;
  306. }
  307. $hcitool = qx(which hcitool);
  308. chomp $hcitool;
  309. if( -x "$hcitool")
  310. {
  311. $return = qx(hcitool name $address 2>/dev/null);
  312. }
  313. else
  314. {
  315. $write_handle->send("error\n") if(defined($write_handle));
  316. }
  317. $querylocker = gettimeofday();
  318. }
  319. chomp $return;
  320. if(not $return =~ /^\s*$/)
  321. {
  322. $write_handle->send("present;$return\n") if(defined($write_handle));
  323. }
  324. else
  325. {
  326. $write_handle->send("absence\n") if(defined($write_handle));
  327. }
  328. $nextrun = gettimeofday() + $timeout;
  329. }
  330. }
  331. sleep 1;
  332. }
  333. }
  334. delete($queues{threads->tid()}) if(exists($queues{threads->tid()}));
  335. }
  336. sub timestamp()
  337. {
  338. return POSIX::strftime("%Y-%m-%d %H:%M:%S",localtime);
  339. }
  340. sub Log($$)
  341. {
  342. my ($loglevel, $message) = @_;
  343. my $thread = 0;
  344. if($message =~ /^\d+\|/)
  345. {
  346. ($thread, $message) = split("\\|", $message);
  347. }
  348. if($loglevel <= $opt_v)
  349. {
  350. if($opt_l)
  351. {
  352. open(LOGFILE, ">>$opt_l") or die ("could not open logfile: $opt_l");
  353. }
  354. else
  355. {
  356. open (LOGFILE, ">&STDOUT") or die("cannot open STDOUT");
  357. }
  358. print LOGFILE "\r".timestamp()." - ".($opt_v >= 2 ? ($thread > 0 ? "(Thread $thread)" : "(Main Thread)")." - ":"").$message."\n";
  359. close(LOGFILE);
  360. }
  361. }