#!/usr/bin/perl -w

=head1 NAME

dh_systemd_start - start/stop/restart systemd unit files

=cut

use strict;
use Debian::Debhelper::Dh_Lib;
use File::Find;
use Text::ParseWords qw(shellwords); # in core since Perl 5
use Cwd qw(getcwd abs_path);

=head1 SYNOPSIS

B<dh_systemd_start> [S<I<debhelper options>>] [B<--restart-after-upgrade>] [B<--no-restart-on-upgrade>] [S<I<unit file> ...>]

=head1 DESCRIPTION

B<dh_systemd_start> is a debhelper program that is responsible for
starting/stopping or restarting systemd unit files in case no corresponding
sysv init script is available.

As with B<dh_installinit>, the unit file is stopped before
upgrades and started afterwards (unless B<--restart-after-upgrade> is
specified, in which case it will only be restarted after the upgrade).
This logic is not used when there is a corresponding SysV init script
because invoke-rc.d performs the stop/start/restart in that case.

=head1 OPTIONS

=over 4

=item B<--restart-after-upgrade>

Do not stop the unit file until after the package upgrade has been completed.
This is different than the default behavior, which stops the unit file in the
F<prerm> and starts it again in the F<postinst> maintscript.

This can be useful for daemons that should not have a possibly long
downtime during upgrade. But you should make sure that the daemon will not
get confused by the package being upgraded while it's running before using
this option.

=item B<-r>, B<--no-restart-on-upgrade>

Do not stop service on upgrade.

=item B<--no-start>

Do not start the unit file after upgrades and after initial installation (the
latter is only relevant for services without a corresponding init script).

=back

=head1 NOTES

Note that this command is not idempotent. L<dh_prep(1)> should be called
between invocations of this command (with the same arguments). Otherwise, it
may cause multiple instances of the same text to be added to maintainer
scripts.

Note that B<dh_systemd_start> should be run after B<dh_installinit> so that it
can detect corresponding SysV init scripts. The default sequence in B<dh> does
the right thing, this note is only relevant when you are calling
B<dh_systemd_start> manually.

=cut

init(options => {
	"r" => \$dh{R_FLAG},
	"no-restart-on-upgrade" => \$dh{R_FLAG},
	"no-start" => \$dh{NO_START},
	"R|restart-after-upgrade" => \$dh{RESTART_AFTER_UPGRADE},
	"no-also" => \$dh{NO_ALSO},
});

# Extracts the Also= or Alias= line(s) from a unit file.
# In case this produces horribly wrong results, you can pass --no-also, but
# that should really not be necessary. Please report bugs to
# pkg-systemd-maintainers.
sub extract_key {
	my ($unit_path, $key) = @_;
	my @values;
	my $fh;

	if ($dh{NO_ALSO}) {
		return @values;
	}

	if (!open($fh, '<', $unit_path)) {
		warning("Cannot open($unit_path) for extracting the Also= line(s)");
		return;
	}
	while (my $line = <$fh>) {
		chomp($line);

		if ($line =~ /^\s*$key=(.+)$/i) {
			@values = (@values, shellwords($1));
		}
	}
	close($fh);
	return @values;
}

foreach my $package (@{$dh{DOPACKAGES}}) {
	my $tmpdir = tmpdir($package);
	my @installed_units;
	my @units;
	my %aliases;

	my $oldcwd = getcwd();
	find({
		wanted => sub {
			my $name = $File::Find::name;
			return unless -f;
			return unless $name =~ m,^$tmpdir/lib/systemd/system/[^/]+$,;
			if (-l) {
				my $target = abs_path(readlink());
				$target =~ s,^$oldcwd/,,g;
				$aliases{$target} = [ $_ ];
			} else {
				push @installed_units, $name;
			}
		},
	}, $tmpdir);
	chdir($oldcwd);

	# Handle either only the unit files which were passed as arguments or
	# all unit files that are installed in this package.
	my @args = @ARGV > 0 ? @ARGV : @installed_units;

	# This hash prevents us from looping forever in the following while loop.
	# An actual real-world example of such a loop is systemd’s
	# systemd-readahead-drop.service, which contains
	# Also=systemd-readahead-collect.service, and that file in turn
	# contains Also=systemd-readahead-drop.service, thus forming an endless
	# loop.
	my %seen;

	# We use while/shift because we push to the list in the body.
	while (@args) {
		my $name = shift @args;
		my $base = basename($name);

		# Try to make the path absolute, so that the user can call
		# dh_installsystemd bacula-fd.service
		if ($base eq $name) {
			# NB: This works because @installed_units contains
			# files from precisely one directory.
			my ($full) = grep { basename($_) eq $base } @installed_units;
			if (defined($full)) {
				$name = $full;
			} else {
				warning(qq|Could not find "$name" in the /lib/systemd/system directory of $package. | .
					qq|This could be a typo, or using Also= with a service file from another package. | .
					qq|Please check carefully that this message is harmless.|);
			}
		}

		# Skip template service files like e.g. getty@.service.
		# Enabling, disabling, starting or stopping those services
		# without specifying the instance (e.g. getty@ttyS0.service) is
		# not useful.
		if ($name =~ /\@/) {
			next;
		}

		# Handle all unit files specified via Also= explicitly.
		# This is not necessary for enabling, but for disabling, as we
		# cannot read the unit file when disabling (it was already
		# deleted).
		my @also = grep { !exists($seen{$_}) } extract_key($name, 'Also');
		$seen{$_} = 1 for @also;
		@args = (@args, @also);

		push @{$aliases{$name}}, $_ for extract_key($name, 'Alias');
		my @sysv = grep {
				my $base = $_;
				$base =~ s/\.(?:service|socket)$//g;
				-f "$tmpdir/etc/init.d/$base"
			} ($base, @{$aliases{$name}});
		if (@sysv == 0 && !grep { $_ eq $name } @units) {
			push @units, $name;
		}
	}

	next if @units == 0;

	# The $package and $sed parameters are always the same.
	# This wrapper function makes the following logic easier to read.
	my $sd_autoscript = sub {
		my ($script, $filename) = @_;
		my $unitargs = join(" ", map { basename($_) } @units);
		autoscript($package, $script, $filename, "s/#UNITFILES#/$unitargs/");
	};

	if ($dh{RESTART_AFTER_UPGRADE}) {
		$sd_autoscript->("postinst", "postinst-systemd-restart");
	} elsif (!$dh{NO_START}) {
		# We need to stop/start before/after the upgrade.
		$sd_autoscript->("postinst", "postinst-systemd-start");
	}

	$sd_autoscript->("postrm", "postrm-systemd-reload-only");

	if ($dh{R_FLAG} || $dh{RESTART_AFTER_UPGRADE}) {
		# stop service only on remove
		$sd_autoscript->("prerm", "prerm-systemd-restart");
	} elsif (!$dh{NO_START}) {
		# always stop service
		$sd_autoscript->("prerm", "prerm-systemd");
	}
}

=head1 SEE ALSO

L<debhelper(7)>

=head1 AUTHORS

pkg-systemd-maintainers@lists.alioth.debian.org

=cut
