Implement signals

Minimum viable product. TERM closes gracefully, KILL just removes PID, and USR1 rotates immediately and restarts timer.

Other changes:
- Whitespace
- Implemented using lazy, excessive use of global variables. This needs to be improved.
- Comment blocks
This commit is contained in:
John Mertz 2022-07-23 03:43:34 -04:00
parent 3f07586ae9
commit 3f54f5f4bc
1 changed files with 244 additions and 202 deletions

View File

@ -2,10 +2,14 @@
use strict; use strict;
use warnings; use warnings;
use POSIX;
use lib "$ENV{HOME}/perl5/lib/perl5"; use lib "$ENV{HOME}/perl5/lib/perl5";
our $debug = 1; # For testing, will output configuration and errors if true our $debug = 1; # For testing, will output configuration and errors if true
our %settings;
our @e;
our $pidfile = "/tmp/$ENV{USER}-wallpaper.pid";
use AnyEvent::Sway; use AnyEvent::Sway;
our $s = AnyEvent::Sway->new(); our $s = AnyEvent::Sway->new();
@ -15,7 +19,7 @@ die "No outputs detected.\n" unless (scalar(@$o));
sub usage() sub usage()
{ {
print("$0 [output(s)] [-d|--daemon]\n print("$0 [output(s)] [-d|--daemon]\n
Sets a wallpaper by cropping an appropriate sized section of a larger image.\n Sets a wallpaper by cropping an appropriate sized section of a larger image.\n
All arguments other than the below are assumed to be output names, as defined All arguments other than the below are assumed to be output names, as defined
in my sway/displays.pl script. If no outputs are provided, all available that in my sway/displays.pl script. If no outputs are provided, all available that
@ -24,268 +28,306 @@ are currently enabled will be set.\n
-p <path> Default: $ENV{HOME}/wallpapers\n -p <path> Default: $ENV{HOME}/wallpapers\n
--daemon=N Configures the wallpaper to be periodically rotated for all --daemon=N Configures the wallpaper to be periodically rotated for all
-d N of the given outputs. N indicates the number of seconds between -d N of the given outputs. N indicates the number of seconds between
each rotation, if provided (default: 300).\n each rotation, if provided (default: 300).\n
--help This menu --help This menu
-h\n"); -h\n");
exit(0); exit(0);
} }
# SIGUSR reset the timer, force immediate reload, continue looping
$SIG{USR1} = sub { $SIG{USR1} = sub {
# TODO: Update the timeout method so that the daemon will simply run the job as a function. alarm 0;
# when SIGUSR1 is received, force the alarm to expire immediately in order to rotate the image without delay. run(%settings);
print "This should rotate the image, but it doesn't yet";
}; };
# SIGTERM reset the timer, clean pid, and allow for the main loop to finish run
$SIG{TERM} = sub { $SIG{TERM} = sub {
clean(); alarm 0;
exit(0); clean();
$settings{daemon} = 0;
}; };
$SIG{INT} = sub { # SIGKILL clean pid then exit immediately
clean(); $SIG{KILL} = sub {
exit(0); clean();
exit(0);
}; };
sub clean sub clean
{ {
my $pidfile = "/var/run/$ENV{HOME}-wallpaper.pid"; if (-e $pidfile) {
if (-e $pidfile) { open (my $fh, '<', $pidfile);
open (my $fh, '<', $pidfile); my $p = <$fh>;
my $p = <$fh>; close($fh);
chomp $p; chomp $p;
kill($p); kill($p);
unlink($pidfile); unlink($pidfile);
} }
} }
sub choose_image sub choose_image
{ {
my $path = shift; my @w = glob("$settings{path}/*");
return undef unless (scalar(@w));
my @w = glob("$path/*"); my @i;
return undef unless (scalar(@w)); foreach (@w) {
if (-d "$settings{path}/$_") {
next;
}
if ($_ =~ m/\.(png|jpg)$/) {
push(@i,$_);
}
}
my @i; return $i[rand(scalar(@i))] || return undef;
foreach (@w) {
if (-d "$path/$_") {
next;
}
if ($_ =~ m/\.(png|jpg)$/) {
push(@i,$_);
}
}
return $i[rand(scalar(@i))] || return undef;
} }
sub get_size sub get_size
{ {
my $target = shift; my $target = shift;
foreach my $output (@$o) { foreach my $output (@$o) {
if ($output->{'name'} eq $target) { if ($output->{'name'} eq $target) {
return ( return (
#$output->{'current_mode'}->{'width'}, #$output->{'current_mode'}->{'width'},
#$output->{'current_mode'}->{'height'} #$output->{'current_mode'}->{'height'}
$output->{'rect'}->{'width'}, $output->{'rect'}->{'width'},
$output->{'rect'}->{'height'} $output->{'rect'}->{'height'}
); );
} }
} }
return undef; return undef;
} }
sub crop sub crop
{ {
my $image = shift; my $image = shift;
my $ow = shift; my $ow = shift;
my $oh = shift; my $oh = shift;
my $cropped = $image; my $cropped = $image;
$cropped =~ s#^.*/([^/]*)\.([^\.]+)$#$1-cropped.$2#; $cropped =~ s#^.*/([^/]*)\.([^\.]+)$#$1-cropped.$2#;
use Image::Magick; use Image::Magick;
my $im = Image::Magick->new(); my $im = Image::Magick->new();
$im->Read($image); $im->Read($image);
my ($iw, $ih) = $im->Get("columns", "rows"); my ($iw, $ih) = $im->Get("columns", "rows");
# Return full size if it is smaller in either dimension then the output # Return full size if it is smaller in either dimension then the output
if ($iw <= $ow || $ih <= $oh) { if ($iw <= $ow || $ih <= $oh) {
print "Not cropping $image because it is too small\n"; print "Not cropping $image because it is too small\n";
$cropped =~ s#-cropped\.([^\.]+)$#\.$1#; $cropped =~ s#-cropped\.([^\.]+)$#\.$1#;
use File::Copy; use File::Copy;
File::Copy::copy($image,$cropped); File::Copy::copy($image,$cropped);
return $cropped if (-e $cropped); return $cropped if (-e $cropped);
return undef; return undef;
} }
print "Image size: $iw $ih\n"; print "Image size: $iw $ih\n";
print "output size: $ow $oh\n"; print "output size: $ow $oh\n";
my ($x, $y); my ($x, $y);
$x = int(rand($iw-$ow)); $x = int(rand($iw-$ow));
$y = int(rand($ih-$oh)); $y = int(rand($ih-$oh));
print "Cropping $image ${ow}x${oh}+${x}+${y}\n"; print "Cropping $image ${ow}x${oh}+${x}+${y}\n";
my $err = $im->Crop(geometry=>"${ow}x${oh}+${x}+${y}"); my $err = $im->Crop(geometry=>"${ow}x${oh}+${x}+${y}");
die "$err" if ($err); die "$err" if ($err);
print "Writing $cropped\n"; print "Writing $cropped\n";
$err = $im->Write($cropped); $err = $im->Write($cropped);
die "$err" if ($err); die "$err" if ($err);
return $cropped if ( -e $cropped ); return $cropped if ( -e $cropped );
} }
sub get_active sub get_active
{ {
my @targets; my @targets;
foreach (@$o) { foreach (@$o) {
push (@targets, $_->{name}); push (@targets, $_->{name});
} }
return @targets; return @targets;
} }
sub set_background sub set_background
{ {
my $target = shift || return "No target or image provided"; my $target = shift || return "No target or image provided";
my $cropped = shift || return "No image provided"; my $cropped = shift || return "No image provided";
# TODO get fallback from javascript # TODO get fallback from javascript
my $cmd = "output $target background $cropped fill #000000"; my $cmd = "output $target background $cropped fill #000000";
print "Running $cmd\n"; print "Running $cmd\n";
my $ret = $s->message(0,$cmd)->recv; my $ret = $s->message(0,$cmd)->recv;
if ($ret->[0]->{success}) { if ($ret->[0]->{success}) {
print "Success!\n"; print "Success!\n";
return undef; return undef;
} }
return "Failed to run Sway IPC command '$cmd'"; return "Failed to run Sway IPC command '$cmd'";
} }
sub run
{
my @err;
# Local copy of targets so that it will re-check active every time
my @t = $settings{targets} if (defined($settings{targets}));
unless (scalar(@t)) {
@t = get_active();
push(@err, "No target outputs") unless (scalar(@t));
}
foreach my $a (@t) {
my $image = choose_image($settings{path});
if (defined($image)) {
if ( -r "$image" ) {
print "selected $image\n";
my $cropped;
if ($settings{crop}) {
if ($settings{debug}) {
print "Cropping image for '$a' using '$image'\n";
}
my ($ow, $oh) = get_size($a);
$cropped = crop($image, $ow, $oh);
unless ($cropped) {
push(@err, "Failed to generate cropped image") unless ($cropped);
next;
}
} else {
$cropped = $image;
}
my $e = set_background($a,$cropped);
push(@err, $e) if (defined($e));
if ($settings{crop}) {
print "Deleting $cropped\n";
#unlink($cropped) || push(@err, "Failed to delete $cropped after setting: $!");
}
} else {
push(@err, "$a: Unable to read image $image");
}
} else {
push(@err, "$a: Unable to select image from $settings{path}");
}
}
if ($settings{debug} && $settings{daemon}) {
print STDERR join("\n",@err);
} else {
@e = @err;
}
}
################################################################################
# Collect arguments
################################################################################
my @targets; my @targets;
my $daemon = 0; my $daemon;
my $delay = 300; my $delay;
my $crop = 1; my $crop;
my $path; my $path;
while (my $arg = shift(@ARGV)) { while (my $arg = shift(@ARGV)) {
if ($arg eq '-h' || $arg eq '--help') { if ($arg eq '-h' || $arg eq '--help') {
usage(); usage();
} elsif ($arg =~ m/^\-\-path=?(.+)$/) { } elsif ($arg =~ m/^\-\-path=?(.+)$/) {
die "Redundant argument '$arg'. Wallpaper path already set.\n" if ($path); die "Redundant argument '$arg'. Wallpaper path already set.\n" if ($path);
$path = $1; $path = $1;
} elsif ($arg eq '-p') { } elsif ($arg eq '-p') {
die "Redundant argument '$arg'. Wallpaper path already set.\n" if ($path); die "Redundant argument '$arg'. Wallpaper path already set.\n" if ($path);
$path = shift(@ARGV); $path = shift(@ARGV);
} elsif ($arg =~ m/^\-\-daemon=?(.+)$/) { } elsif ($arg =~ m/^\-\-daemon=?(.+)$/) {
die "Redundant argument '$arg'. Daemon mode already set.\n" if ($daemon); die "Redundant argument '$arg'. Daemon mode already set.\n" if ($daemon);
$delay = $1; $delay = $1;
$daemon = 1; $daemon = 1;
} elsif ($arg eq '-d') { } elsif ($arg eq '-d') {
die "Redundant argument '$arg'. Daemon mode already set.\n" if ($daemon); die "Redundant argument '$arg'. Daemon mode already set.\n" if ($daemon);
if ($ARGV[0] =~ m/^\d+$/) { if ($ARGV[0] =~ m/^\d+$/) {
$delay = shift(@ARGV); $delay = shift(@ARGV);
} }
$daemon = 1; $daemon = 1;
} elsif ($arg eq '--nocrop' || $arg eq '-') { } elsif ($arg eq '--nocrop' || $arg eq '-') {
die "Redundant argument '$arg'. No-crop already set.\n" unless ($crop); die "Redundant argument '$arg'. No-crop already set.\n" unless ($crop);
} else { } else {
push(@targets,$arg); push(@targets,$arg);
} }
} }
die "Invalid rotation delay: $delay" unless ($delay =~ m/^\d+$/); ################################################################################
# Validate arguments
################################################################################
if ($path) { die "Invalid rotation delay: $delay" if (defined($delay) && $delay !~ m/^\d+$/);
die "Invalid wallpaper path '$path'. Not a directory." unless (-d $path);
} else { die "Invalid wallpaper path '$path'. Not a directory." if (defined($path) && !-d $path);
$path = "$ENV{HOME}/wallpapers";
}
if (scalar(@targets)) { if (scalar(@targets)) {
my @kept; my @kept;
foreach my $t (@targets) { foreach my $t (@targets) {
my $hit = 0; my $hit = 0;
foreach (@$o) { foreach (@$o) {
if ($_->{name} eq $t) { if ($_->{name} eq $t) {
push(@kept, $t); push(@kept, $t);
$hit++; $hit++;
last; last;
} }
} }
print STDERR "Requested output '$t' not found\n" unless ($hit); print STDERR "Requested output '$t' not found\n" unless ($hit);
} }
die "None of the requested outputs are active" unless (scalar(@kept)); die "None of the requested outputs are active" unless (scalar(@kept));
@targets = @kept; @targets = @kept;
} }
if ($daemon) { $settings{targets} = @targets || undef;
my $p = fork(); $settings{daemon} = $daemon || 0;
if ($p) { $settings{path} = $path || "$ENV{HOME}/wallpapers";
my $pidfile = "/var/run/$ENV{HOME}-wallpaper.pid"; $settings{crop} = $crop || 1;
open(my $fh, ">", $pidfile) || die "Failed to open pidfile: $pidfile"; $settings{delay} = $delay || 300;
print $fh "Daemonized as PID: $p\n" || die "Failed to write pid ($p) to pidfile $pidfile"; $settings{debug} = $debug || 0;
close($fh);
exit(0); ################################################################################
} # For if daemonized
################################################################################
if ($settings{daemon}) {
my $p = fork();
if ($p) {
if (open(my $fh, ">", $pidfile)) {
print $fh "$p" || die "Failed to write pid ($p) to pidfile $pidfile";
close($fh);
} else {
print "Failed to open pidfile: $pidfile: $!\n";
}
exit(0);
}
#setpgrp;
#setsid;
umask 0;
# TODO: configure systemD logging
} }
if ($debug) { ################################################################################
print "Initial configuration:\n"; # Main
print " Targets: ( " . ( join(" ",@targets) || "All active" ) . " )\n"; ################################################################################
print " Daemon: $daemon\n";
print " Path: $path\n";
}
my @e;
do { do {
my @err; run(%settings);
# Local copy of targets so that it will re-check active every time if ($settings{daemon}) {
my @t = @targets; my $normal = "reload wallpaper";
unless (scalar(@t)) { eval {
@t = get_active(); $SIG{ALRM} = sub { print "$normal\n" };
push(@err, "No target outputs") unless (scalar(@t)); alarm $settings{delay};
} POSIX::pause();
foreach my $a (@t) { alarm 0;
my $image = choose_image($path); };
if (defined($image)) { if ($@ && $@ !~ quotemeta($normal)) {
if ( -r "$image" ) { if ($settings{debug}) {
print "selected $image\n"; print $normal;
my $cropped; }
if ($crop) { }
if ($debug) { }
print "Cropping image for '$a' using '$image'\n"; } while ($settings{daemon});
}
my ($ow, $oh) = get_size($a); ################################################################################
$cropped = crop($image, $ow, $oh); # If we made it to here, it was not daemonized. Output errors and exit
unless ($cropped) { ################################################################################
push(@err, "Failed to generate cropped image") unless ($cropped);
next;
}
} else {
$cropped = $image;
}
my $e = set_background($a,$cropped);
push(@err, $e) if (defined($e));
if ($crop) {
print "Deleting $cropped\n";
#unlink($cropped) || push(@err, "Failed to delete $cropped after setting: $!");
}
} else {
push(@err, "$a: Unable to read image $image");
}
} else {
push(@err, "$a: Unable to select image from $path");
}
}
if ($debug && $daemon) {
print STDERR join("\n",@err);
sleep($delay);
} else {
@e = @err;
}
} while ($daemon);
if (scalar(@e)) { if (scalar(@e)) {
die "Failure while not running as daemon:\n" . die "Failure while not running as daemon:\n" .
join("\n",@e); join("\n",@e);
} }
exit(0); exit(0);