summaryrefslogtreecommitdiff
path: root/vdrpbd
blob: 0fff3f6b1beb65bc04aacd43789d557aa0e663bb (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
#!/usr/bin/perl
#    vdrpbd - A daemon to handle ACPI power button event on VDR systems
#    Copyright (C) 2020  Manuel Reimer <manuel.reimer@gmx.de>
#
#    This program 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 3 of the License, or
#    (at your option) any later version.
#
#    This program 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 this program.  If not, see <http://www.gnu.org/licenses/>.

use strict;
use Pod::PlainText;
use Getopt::Std;
use POSIX;
use threads;
use Thread::Queue;
use Sys::Syslog qw(:standard :macros);
use Fcntl qw(LOCK_EX LOCK_NB LOCK_UN);
use IO::Socket::IP;
use FileHandle;
use constant {EVIOCGRAB => 0x40044590, EV_KEY => 1, KEY_POWER => 116};
my $HAVE_DBUS = eval {require Net::DBus;};

my $VERSION = '2.0.1';
my $PROGNAME = 'vdrpbd';
my $PIDFILE = '/var/run/vdrpbd.pid';
my $CFGFILE = '/etc/vdrpbd.conf';
my $FHPID;
my %CONF = (
  ER_COUNT => 4, # Number of keypresses and ...
  ER_TIME => 3,  # ... timerange in seconds for emergency reboot
  TARGET => 'svdrp' # Target to send the power button event to
);
my $KEYQUEUE = Thread::Queue->new();

# Main code (in main thread)
{
  # Prepare logging stuff
  openlog($PROGNAME, 'pid', LOG_DAEMON);
  $SIG{__WARN__} = sub {syslog(LOG_WARNING, @_);};
  $SIG{__DIE__} =  sub {die(@_) if ($^S); syslog(LOG_ERR, @_); exit(1);};

  # Read parameters
  my %opts;
  $Getopt::Std::STANDARD_HELP_VERSION = 1;
  getopts('f', \%opts);

  # Read config
  ParseConfig();
  $CONF{TARGET} = 'dbus' if ($CONF{USE_DBUS}); # Legacy "USE_DBUS" option
  if ($CONF{TARGET} !~ /^(svdrp|dbus|kodi)$/) {
    die("Invalid value for TARGET in $CFGFILE!\n");
  }
  if ($CONF{TARGET} eq 'dbus' && !$HAVE_DBUS) {
    die("DBus support requested but no Net::DBus module present!\n");
  }
  if ($CONF{ER_COUNT} !~ /^[0-9]+$/ || $CONF{ER_COUNT} < 2) {
    die("Invalid value for ER_COUNT in $CFGFILE!\n");
  }
  if ($CONF{ER_TIME} !~ /^[0-9]+$/ || $CONF{ER_TIME} < 1) {
    die("Invalid value for ER_TIME in $CFGFILE!\n");
  }

  # Prepare environment
  chdir('/');
  Daemonize() unless ($opts{f});

  # Register cleanup stuff
  $SIG{INT} = \&Cleanup;
  $SIG{TERM} = \&Cleanup;

  # Connect to the power button devices
  # https://www.cs.ait.ac.th/~on/O/oreilly/perl/cookbook/ch07_14.htm
  # We remember all file handles and create a bitmask for "select" from them
  my @devices = GetButtonDevices();
  my @fhdevs;
  my $rin = '';
  foreach my $device (@devices) {
    open(my $fhdev, '<', $device) or die("Failed to open $device\n");
    vec($rin, fileno($fhdev), 1) = 1;
    push(@fhdevs, $fhdev);


    # VDR reacts with "Press any key to cancel shutdown" if we send our
    # shutdown request. If we let the KEY_POWER through to the X server, then
    # this *is* a key press and depending on timing it may cancel shutdown.
    # "Kodi mode" is just to not block the key from reaching the X server.
    # Kodi properly handles KEY_POWER on its own.
    if ($CONF{TARGET} ne 'kodi') {
      ioctl($fhdev, EVIOCGRAB, 1) or warn("Failed to get exclusive access!\n");
    }
  }

  # Register with systemd if needed/possible
  SystemdInhibit() if ($HAVE_DBUS && HaveSystemd());

  # Run worker thread
  threads->new(\&KeyProcessor)->detach();

  # Process keypresses
  my $struct_input_event = 'L!L!SSl';
  my @btnhist;
  # Wait until one of the devices gets ready for read
  while (select(my $rout = $rin, undef, undef, undef)) {
    # Read one event from the first "readable" device we find
    my $event = 0;
    foreach my $fhdev (@fhdevs) {
      if (vec($rout, fileno($fhdev), 1)) {
        sysread($fhdev, $event, length(pack($struct_input_event)));
        last;
      }
    }
    next unless($event);

    my ($tv_sec, $tv_usec, # <<-- timeval
        $type, $code, $value) = unpack($struct_input_event, $event);
    next unless ($type == EV_KEY && $code == KEY_POWER && $value == 0);

    # Info message to syslog
    syslog(LOG_INFO, 'Power key pressed.');

    # Detect emergency reboot case
    push(@btnhist, $tv_sec);
    if (@btnhist == $CONF{ER_COUNT} &&
        $tv_sec - shift(@btnhist) <= $CONF{ER_TIME}) {
      syslog(LOG_INFO, 'Initiating user-requested emergency reboot!');
      system('/sbin/shutdown', '-r', 'now');
    }

    # Add keypress to queue for worker thread to process.
    # Don't enqueue more than 4 keypresses.
    $KEYQUEUE->enqueue(1) if ($KEYQUEUE->pending() < 4);
  }

  # Close and cleanup
  foreach my $fhdev (@fhdevs) {
    close($fhdev);
  }
  Cleanup();
}

# Worker thread. Tries to forward enqueued keypresses to VDR/Kodi.
sub KeyProcessor {
  while ($KEYQUEUE->dequeue()) {
    if ($CONF{TARGET} eq 'dbus') {
      SendDBus();
    }
    elsif ($CONF{TARGET} eq 'svdrp') {
      SendSVDRP();
    }
  }
}

# Cleanup routine
sub Cleanup {
  if ($FHPID) {
    flock($FHPID, LOCK_UN);
    close($FHPID);
    unlink($PIDFILE);
  }
  exit(0);
}

sub VERSION_MESSAGE {
  print "$PROGNAME $VERSION\n";
}

sub HELP_MESSAGE {
  # Print out the built-in POD documentation in case of --help parameter
  Pod::PlainText->new(sentence => 0)->parse_from_file($0);
}

sub ParseConfig {
  return unless (-s $CFGFILE);
  open(my $fh, '<', $CFGFILE) or die("Failed to open $CFGFILE\n");
  while (my $line = <$fh>) {
    my ($pref, $value) = $line =~ /^\s*([A-Z_]+)\s*=\s*(.+)/ or next;
    $CONF{$pref} = $value;
  }
  close($fh);
}

sub Daemonize {
  # Fork to background
  my $pid = fork();
  die("Forking to background failed\n") unless (defined($pid));

  # Exit parent process.
  exit(0) if ($pid);

  # Close open file handles to old terminal
  close(STDIN);
  close(STDOUT);
  close(STDERR);

  # Write pidfile
  sysopen($FHPID, $PIDFILE, O_CREAT|O_RDWR) or die("Opening pidfile failed\n");
  flock($FHPID, LOCK_EX|LOCK_NB) or die("Daemon already running\n");
  truncate($FHPID, 0);
  print $FHPID "$$\n";

  # Get process group owner
  setsid();
}

sub HaveSystemd {
  # We simply test whether the systemd cgroup hierarchy is mounted
  my @a = lstat('/sys/fs/cgroup') or return 0;
  my @b = lstat('/sys/fs/cgroup/systemd') or return 0;
  return $a[0] != $b[0];
}

# Returns a list of possible "power button devices" found on this system
sub GetButtonDevices {
  # Power buttons to check for
  my @devicepaths = (
    '/sys/devices/LNXSYSTM:00/LNXSYBUS:00/PNP0C0E:00/input',
    '/sys/devices/LNXSYSTM:00/LNXPWRBN:00/input',
    '/sys/devices/LNXSYSTM:00/LNXSYBUS:00/PNP0C0C:00/input'
  );

  my @basepaths = grep {-d $_} @devicepaths or die("No power button found\n");
  my @result;
  foreach my $basepath (@basepaths) {
    opendir(my $dh, $basepath) or next;
    my ($input) = grep(/^input/, readdir($dh)) or next;
    closedir($dh);
    opendir($dh, "$basepath/$input") or next;
    my ($event) = grep(/^event/, readdir($dh)) or next;
    closedir($dh);
    push(@result, "/dev/input/$event");
  }

  die("No power button found\n") if (@result == 0);

  return @result;
}

# Establishes local TCP connection.
# Parameters:
#   aPort: Port to connect to
# Return values:
#   Connected socket on success
#   undef on error
sub ConnectTCP {
  my $timeout = 15; # Socket timeout in seconds
  my ($aPort) = @_;

  my $sock = IO::Socket::IP->new(
    PeerHost => 'localhost',
    PeerPort => $aPort,
    Type     => SOCK_STREAM,
    Timeout  => $timeout
  );

  # Connection failed
  if (!$sock) {
    warn("ConnectTCP: $!");
    return;
  }

  $sock->autoflush(1);
  return $sock;
}

sub SendSVDRP {
  my $port = getservbyname('svdrp', 'tcp') || 6419;
  my $sh = ConnectTCP($port) or return;

  # Send power button event
  print $sh "HITK POWER\nQUIT\n"; # Send full command sequence at once!
  my @reply = <$sh>;
  if ($!) { # Read timed out
    warn("svdrp: $!");
    return;
  }
  close($sh);

  # Process messages returned by VDR
  foreach my $msg (@reply) {
    $msg =~ s/\r$//;
    warn("svdrp: $msg") if ($msg =~ /^5/);
  }
}

# This one requires the "dbus2vdr-plugin" to be installed.
sub SendDBus {
  eval {
    my $bus = Net::DBus->system();
    my $service = $bus->get_service('de.tvdr.vdr');
    my $object = $service->get_object('/Remote', 'de.tvdr.vdr.remote');
    $object->HitKey('POWER');
  } or warn("SendDBus: $@");
}

sub SystemdInhibit {
  # HACK... Add support for UNIX FD return values to Net::DBus.
  # 2013-01-24: Mailed patch to module developer
  # 2013-02-07: First reply from developer --> Patch will be added after review
  # 2013-03-27: Sent mail asking for an update about current status
  # 2013-04-05: https://gitorious.org/net-dbus/net-dbus/commit/5bf227d
  unless (exists $Net::DBus::Binding::Introspector::simple_type_rev_map{ord('h')}) {
    $Net::DBus::Binding::Introspector::simple_type_rev_map{ord('h')} = 'unixfd';
    $Net::DBus::Binding::Introspector::simple_type_map{'unixfd'} = ord('h');
    my $orig_get = \&Net::DBus::Binding::Iterator::get;
    *Net::DBus::Binding::Iterator::get = sub {
      my ($self, $type) = @_;
      return ($type == ord('h')) ? $self->get_int32() : $orig_get->(@_);
    };
  }

  # Try to inhibit the power key.
  eval {
    my $bus = Net::DBus->system();
    my $logind = $bus->get_service('org.freedesktop.login1');
    my $manager = $logind->get_object('/org/freedesktop/login1',
                                      'org.freedesktop.login1.Manager');
    $manager->Inhibit('handle-power-key', $PROGNAME, '', 'block');
  } or warn("systemd-inhibit: $@");
}

__END__

=head1 NAME

vdrpbd - A daemon to handle ACPI power button event on VDR systems

=head1 SYNOPSIS

B<vdrpbd> S<[ B<-f> ]>

=head1 DESCRIPTION

B<vdrpbd> is a ACPI power button handling daemon, which has been created with a VDR-based HTPC in mind. In such setups, the power button on the front should be forwarded as event to the VDR software and no hard shutdown should be triggered. This is where B<vdrpbd> comes in. It listens on the relevant input device and forwards button presses to the VDR process.

The usual downside of this is, that a hanging VDR process makes the power button useless for shutting down the system cleanly. You either have to do a shutdown via remote access or, if available, via terminal on your TV. To address this issue, B<vdrpbd> has a "emergency reboot" feature. If you press the power button four times within three seconds, then the daemon triggers a system reboot to bring your system back to a working state in a clean, easy and fast way.

=head1 KODI SUPPORT

B<vdrpbd> supports Kodi as frontend for Linux HTPC systems. This doesn't require you to use VDR as PVR backend or any PVR backend at all. To have the power button sent to Kodi, just set B<TARGET> in vdrpbd.conf to B<kodi>.

=head2 Command Switches

Switches include:

=over 5

=item B<-f>

Run B<vdrpbd> in the foreground. Default is to run in the background as "daemon".

=item B<--help>

display this help and exit

=item B<--version>

output version information and exit

=back

=head1 SEE ALSO

vdrpbd.conf(5)