From 3f54f5f4bcf21bc9c4b1a962e77be92268c8205f Mon Sep 17 00:00:00 2001 From: John Mertz Date: Sat, 23 Jul 2022 03:43:34 -0400 Subject: [PATCH] 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 --- sway/wallpaper.pl | 446 +++++++++++++++++++++++++--------------------- 1 file changed, 244 insertions(+), 202 deletions(-) diff --git a/sway/wallpaper.pl b/sway/wallpaper.pl index f013c21..eec013b 100755 --- a/sway/wallpaper.pl +++ b/sway/wallpaper.pl @@ -2,10 +2,14 @@ use strict; use warnings; +use POSIX; use lib "$ENV{HOME}/perl5/lib/perl5"; 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; our $s = AnyEvent::Sway->new(); @@ -15,7 +19,7 @@ die "No outputs detected.\n" unless (scalar(@$o)); 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 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 @@ -24,268 +28,306 @@ are currently enabled will be set.\n -p Default: $ENV{HOME}/wallpapers\n --daemon=N Configures the wallpaper to be periodically rotated for all -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 -h\n"); - exit(0); + exit(0); } +# SIGUSR reset the timer, force immediate reload, continue looping $SIG{USR1} = sub { - # TODO: Update the timeout method so that the daemon will simply run the job as a function. - # when SIGUSR1 is received, force the alarm to expire immediately in order to rotate the image without delay. - print "This should rotate the image, but it doesn't yet"; + alarm 0; + run(%settings); }; +# SIGTERM reset the timer, clean pid, and allow for the main loop to finish run $SIG{TERM} = sub { - clean(); - exit(0); + alarm 0; + clean(); + $settings{daemon} = 0; }; -$SIG{INT} = sub { - clean(); - exit(0); +# SIGKILL clean pid then exit immediately +$SIG{KILL} = sub { + clean(); + exit(0); }; sub clean { - my $pidfile = "/var/run/$ENV{HOME}-wallpaper.pid"; - if (-e $pidfile) { - open (my $fh, '<', $pidfile); - my $p = <$fh>; - chomp $p; - kill($p); - unlink($pidfile); - } + if (-e $pidfile) { + open (my $fh, '<', $pidfile); + my $p = <$fh>; + close($fh); + chomp $p; + kill($p); + unlink($pidfile); + } } sub choose_image { - my $path = shift; + my @w = glob("$settings{path}/*"); + return undef unless (scalar(@w)); - my @w = glob("$path/*"); - return undef unless (scalar(@w)); + my @i; + foreach (@w) { + if (-d "$settings{path}/$_") { + next; + } + if ($_ =~ m/\.(png|jpg)$/) { + push(@i,$_); + } + } - my @i; - foreach (@w) { - if (-d "$path/$_") { - next; - } - if ($_ =~ m/\.(png|jpg)$/) { - push(@i,$_); - } - } - - return $i[rand(scalar(@i))] || return undef; + return $i[rand(scalar(@i))] || return undef; } sub get_size { - my $target = shift; - foreach my $output (@$o) { - if ($output->{'name'} eq $target) { - return ( - #$output->{'current_mode'}->{'width'}, - #$output->{'current_mode'}->{'height'} - $output->{'rect'}->{'width'}, - $output->{'rect'}->{'height'} - ); - } - } - return undef; + my $target = shift; + foreach my $output (@$o) { + if ($output->{'name'} eq $target) { + return ( + #$output->{'current_mode'}->{'width'}, + #$output->{'current_mode'}->{'height'} + $output->{'rect'}->{'width'}, + $output->{'rect'}->{'height'} + ); + } + } + return undef; } sub crop { - my $image = shift; - my $ow = shift; - my $oh = shift; + my $image = shift; + my $ow = shift; + my $oh = shift; - my $cropped = $image; - $cropped =~ s#^.*/([^/]*)\.([^\.]+)$#$1-cropped.$2#; + my $cropped = $image; + $cropped =~ s#^.*/([^/]*)\.([^\.]+)$#$1-cropped.$2#; - use Image::Magick; - my $im = Image::Magick->new(); + use Image::Magick; + my $im = Image::Magick->new(); - $im->Read($image); - my ($iw, $ih) = $im->Get("columns", "rows"); + $im->Read($image); + my ($iw, $ih) = $im->Get("columns", "rows"); - # Return full size if it is smaller in either dimension then the output - if ($iw <= $ow || $ih <= $oh) { - print "Not cropping $image because it is too small\n"; - $cropped =~ s#-cropped\.([^\.]+)$#\.$1#; - use File::Copy; - File::Copy::copy($image,$cropped); - return $cropped if (-e $cropped); - return undef; - } + # Return full size if it is smaller in either dimension then the output + if ($iw <= $ow || $ih <= $oh) { + print "Not cropping $image because it is too small\n"; + $cropped =~ s#-cropped\.([^\.]+)$#\.$1#; + use File::Copy; + File::Copy::copy($image,$cropped); + return $cropped if (-e $cropped); + return undef; + } - print "Image size: $iw $ih\n"; - print "output size: $ow $oh\n"; + print "Image size: $iw $ih\n"; + print "output size: $ow $oh\n"; - my ($x, $y); - $x = int(rand($iw-$ow)); - $y = int(rand($ih-$oh)); + my ($x, $y); + $x = int(rand($iw-$ow)); + $y = int(rand($ih-$oh)); - print "Cropping $image ${ow}x${oh}+${x}+${y}\n"; - my $err = $im->Crop(geometry=>"${ow}x${oh}+${x}+${y}"); - die "$err" if ($err); - print "Writing $cropped\n"; - $err = $im->Write($cropped); - die "$err" if ($err); - return $cropped if ( -e $cropped ); + print "Cropping $image ${ow}x${oh}+${x}+${y}\n"; + my $err = $im->Crop(geometry=>"${ow}x${oh}+${x}+${y}"); + die "$err" if ($err); + print "Writing $cropped\n"; + $err = $im->Write($cropped); + die "$err" if ($err); + return $cropped if ( -e $cropped ); } sub get_active { - my @targets; - foreach (@$o) { - push (@targets, $_->{name}); - } - return @targets; + my @targets; + foreach (@$o) { + push (@targets, $_->{name}); + } + return @targets; } sub set_background { - my $target = shift || return "No target or image provided"; - my $cropped = shift || return "No image provided"; - # TODO get fallback from javascript - my $cmd = "output $target background $cropped fill #000000"; - print "Running $cmd\n"; - my $ret = $s->message(0,$cmd)->recv; - if ($ret->[0]->{success}) { - print "Success!\n"; - return undef; - } - return "Failed to run Sway IPC command '$cmd'"; + my $target = shift || return "No target or image provided"; + my $cropped = shift || return "No image provided"; + # TODO get fallback from javascript + my $cmd = "output $target background $cropped fill #000000"; + print "Running $cmd\n"; + my $ret = $s->message(0,$cmd)->recv; + if ($ret->[0]->{success}) { + print "Success!\n"; + return undef; + } + 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 $daemon = 0; -my $delay = 300; -my $crop = 1; +my $daemon; +my $delay; +my $crop; my $path; - while (my $arg = shift(@ARGV)) { - if ($arg eq '-h' || $arg eq '--help') { - usage(); - } elsif ($arg =~ m/^\-\-path=?(.+)$/) { - die "Redundant argument '$arg'. Wallpaper path already set.\n" if ($path); - $path = $1; - } elsif ($arg eq '-p') { - die "Redundant argument '$arg'. Wallpaper path already set.\n" if ($path); - $path = shift(@ARGV); - } elsif ($arg =~ m/^\-\-daemon=?(.+)$/) { - die "Redundant argument '$arg'. Daemon mode already set.\n" if ($daemon); - $delay = $1; - $daemon = 1; - } elsif ($arg eq '-d') { - die "Redundant argument '$arg'. Daemon mode already set.\n" if ($daemon); - if ($ARGV[0] =~ m/^\d+$/) { - $delay = shift(@ARGV); - } - $daemon = 1; - } elsif ($arg eq '--nocrop' || $arg eq '-') { - die "Redundant argument '$arg'. No-crop already set.\n" unless ($crop); - } else { - push(@targets,$arg); - } + if ($arg eq '-h' || $arg eq '--help') { + usage(); + } elsif ($arg =~ m/^\-\-path=?(.+)$/) { + die "Redundant argument '$arg'. Wallpaper path already set.\n" if ($path); + $path = $1; + } elsif ($arg eq '-p') { + die "Redundant argument '$arg'. Wallpaper path already set.\n" if ($path); + $path = shift(@ARGV); + } elsif ($arg =~ m/^\-\-daemon=?(.+)$/) { + die "Redundant argument '$arg'. Daemon mode already set.\n" if ($daemon); + $delay = $1; + $daemon = 1; + } elsif ($arg eq '-d') { + die "Redundant argument '$arg'. Daemon mode already set.\n" if ($daemon); + if ($ARGV[0] =~ m/^\d+$/) { + $delay = shift(@ARGV); + } + $daemon = 1; + } elsif ($arg eq '--nocrop' || $arg eq '-') { + die "Redundant argument '$arg'. No-crop already set.\n" unless ($crop); + } else { + push(@targets,$arg); + } } -die "Invalid rotation delay: $delay" unless ($delay =~ m/^\d+$/); +################################################################################ +# Validate arguments +################################################################################ -if ($path) { - die "Invalid wallpaper path '$path'. Not a directory." unless (-d $path); -} else { - $path = "$ENV{HOME}/wallpapers"; -} +die "Invalid rotation delay: $delay" if (defined($delay) && $delay !~ m/^\d+$/); + +die "Invalid wallpaper path '$path'. Not a directory." if (defined($path) && !-d $path); if (scalar(@targets)) { - my @kept; - foreach my $t (@targets) { - my $hit = 0; - foreach (@$o) { - if ($_->{name} eq $t) { - push(@kept, $t); - $hit++; - last; - } - } - print STDERR "Requested output '$t' not found\n" unless ($hit); - } - die "None of the requested outputs are active" unless (scalar(@kept)); - @targets = @kept; + my @kept; + foreach my $t (@targets) { + my $hit = 0; + foreach (@$o) { + if ($_->{name} eq $t) { + push(@kept, $t); + $hit++; + last; + } + } + print STDERR "Requested output '$t' not found\n" unless ($hit); + } + die "None of the requested outputs are active" unless (scalar(@kept)); + @targets = @kept; } -if ($daemon) { - my $p = fork(); - if ($p) { - my $pidfile = "/var/run/$ENV{HOME}-wallpaper.pid"; - open(my $fh, ">", $pidfile) || die "Failed to open pidfile: $pidfile"; - print $fh "Daemonized as PID: $p\n" || die "Failed to write pid ($p) to pidfile $pidfile"; - close($fh); - exit(0); - } +$settings{targets} = @targets || undef; +$settings{daemon} = $daemon || 0; +$settings{path} = $path || "$ENV{HOME}/wallpapers"; +$settings{crop} = $crop || 1; +$settings{delay} = $delay || 300; +$settings{debug} = $debug || 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"; - print " Targets: ( " . ( join(" ",@targets) || "All active" ) . " )\n"; - print " Daemon: $daemon\n"; - print " Path: $path\n"; -} +################################################################################ +# Main +################################################################################ -my @e; do { - my @err; - # Local copy of targets so that it will re-check active every time - my @t = @targets; - unless (scalar(@t)) { - @t = get_active(); - push(@err, "No target outputs") unless (scalar(@t)); - } - foreach my $a (@t) { - my $image = choose_image($path); - if (defined($image)) { - if ( -r "$image" ) { - print "selected $image\n"; - my $cropped; - if ($crop) { - if ($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 ($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); + run(%settings); + if ($settings{daemon}) { + my $normal = "reload wallpaper"; + eval { + $SIG{ALRM} = sub { print "$normal\n" }; + alarm $settings{delay}; + POSIX::pause(); + alarm 0; + }; + if ($@ && $@ !~ quotemeta($normal)) { + if ($settings{debug}) { + print $normal; + } + } + } +} while ($settings{daemon}); + +################################################################################ +# If we made it to here, it was not daemonized. Output errors and exit +################################################################################ if (scalar(@e)) { - die "Failure while not running as daemon:\n" . - join("\n",@e); + die "Failure while not running as daemon:\n" . + join("\n",@e); } exit(0);