#!/usr/bin/perl -w

=head1 NAME

dh_installs6 - install s6-rc unit files

=cut

use strict;
use warnings;
use Debian::Debhelper::Dh_Lib;
use File::Find;
use Cwd qw(getcwd abs_path);

our $VERSION = DH_BUILTIN_VERSION;

=head1 SYNOPSIS

B<dh_installs6> [S<I<debhelper options>>]

=head1 DESCRIPTION

B<dh_installs6> is a debhelper program that is responsible for
installing package maintainer supplied s6-rc unit folders.

Generates F<preinst>, F<postinst>, F<prerm> and F<postrm> code blocks for
enabling, disabling and restarting the corresponding s6-rc services, when the
package is installed, updated, or removed. These snippets are added to the
maintainer scripts by L<dh_installdeb(1)>.

A unit is enabled exactly once: when it did not exist on the system before
the package version being installed. F<preinst> records the units already
present under F</usr/lib/s6-rc/sources> (state file
F</var/lib/dh-s6/I<package>.existing>), F<postinst> enables the others. The
decision deliberately does not consult the s6-rc databases: other maintainer
scripts in the same dpkg run (notably B<deb-systemd-helper>, which always
runs C<svctl compile && svctl update>) may have compiled the freshly unpacked
unit into them already, which made the former C<svctl isenabled> check see
"known, disabled" and never enable it.

B<dh_installs6> automatically processes unit folders containing the file
F<producer-for> (services). For changing the generated blocks behaviour use
file hints (e.g. B<.noenable>) for specific unit folders.

To enable this addon in your source package use B<Build-Depends: dh-sequence-s6>
or set `--with s6` in your debian/rules file.

=head1 FILES

=over 4

=item F<debian/I<package>.s6-rc/*/>

If any of those folders exists, they are installed into
F<usr/lib/s6-rc/sources/> in the package build directory.

=item F<debian/I<package>.s6-rc/*/.noenable>

Disable the service(s) on purge, but do not enable them on install.

B<Note> that this option also affects whether the services are
started/restarted. If a service is enabled or was running it will be
started/restarted. Use file B<.norestart> to mitigate that.

=item F<debian/I<package>.s6-rc/*/.norestart>

Do not restart the service after upgrades or start after initial installation.

B<Note> that this option does not affect whether the services are
enabled. Please remember to also use file B<.noenable> if the services
should not be enabled.

=item F<debian/I<package>.s6-rc/*/.permafail>

Opt in to permanent failure on a crash loop. B<dh_installs6> generates a
F<finish> script for that unit:

  #!/usr/bin/execlineb -S3
  s6-permafailon <arguments>
  exit 0

After the configured number of deaths inside the configured window,
B<s6-permafailon>(1) makes F<finish> exit 125, which tells B<s6-supervise>
to stop restarting the service. The service then reads as
C<up=false wantedup=false> in B<s6-svstat>(1) instead of looking healthy
while dying in a loop (C<svctl isup> only reports the s6-rc I<wanted> state
and cannot see a restart loop). Recover with C<svctl restart> and clear the
death tally with B<s6-svdt-clear>(1).

The content of the file is the argument list for B<s6-permafailon>, i.e.
I<seconds> I<deaths> I<events>. An empty file means the default

  120 5 1-255,SIGSEGV,SIGABRT,SIGKILL

(five deaths within two minutes, counting any non-zero exit code and the
usual fatal signals). Choose a generous window for services that legitimately
wait for a late network: permanent failure is deliberately sticky.

Only valid for units of type C<longrun> and only if the unit does not ship a
F<finish> script of its own; either condition raises a build error. Requires
B<s6> 2.7.1.0 or newer on the target system (jammy ships 2.11.1.0, resolute
2.13.1.0).

=back

=head1 NOTES

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.

=cut

init(options => {});

# s6-permafailon arguments used when .permafail is empty: five deaths within
# two minutes, counting every non-zero exit code and the usual fatal signals.
use constant PERMAFAIL_DEFAULT => '120 5 1-255,SIGSEGV,SIGABRT,SIGKILL';

sub slurp_first_line {
	# First non-empty line of a file, whitespace-trimmed; '' if unreadable.
	my ($file) = @_;
	open(my $fh, '<', $file) or return '';
	while (my $line = <$fh>) {
		$line =~ s/\s+$//;
		$line =~ s/^\s+//;
		next unless length $line;
		close($fh);
		return $line;
	}
	close($fh);
	return '';
}

sub quote {
	# Add single quotes around the argument.
	return '\'' . $_[0] . '\'';
}

sub uniq {
	my %seen;
	return grep { !$seen{$_}++ } @_;
}

sub list_installed_service_units {
	my ($tmpdir) = @_;

	my @installed;

	foreach my $unitdir (glob("$tmpdir/usr/lib/s6-rc/sources/*")) {
		next unless -d $unitdir;
		next if -f "$unitdir/consumer-for"; # only service units (not log units)
		push @installed, basename($unitdir);
	}

	return @installed;
}

# Install package maintainer supplied unit folders
foreach my $package (@{$dh{DOPACKAGES}}) {
	my $tmpdir = tmpdir($package);

	# Install all unit folders in the debian/ directory in the
	# form $package.s6-rc/$name.
	my $basedir = "debian/$package.s6-rc";
	my $targetdir = "$tmpdir/usr/lib/s6-rc/sources";

	next unless -d $basedir;

	install_dir($targetdir);
	foreach my $namepath (glob("$basedir/*")) {
		doit("cp", '--reflink=auto', "-r", "-a", $namepath, $targetdir);
	}
}

# Add postinst, prerm, and postrm code blocks to handle activation,
# deactivation, start and stopping of services when the package is
# installed, upgraded or removed.
foreach my $package (@{$dh{DOPACKAGES}}) {
	my $tmpdir = tmpdir($package);
	my (@args, @enable_units, @restart_units);

	my @service_units = list_installed_service_units($tmpdir);

	# find service units that should not be enabled or restarted
	for my $unit (@service_units) {
		my $path = "${tmpdir}/usr/lib/s6-rc/sources/${unit}";
		push @enable_units, $unit unless (-f "$path/.noenable");
		push @restart_units, $unit unless (-f "$path/.norestart");
	}

	# generate ./finish scripts for units that opted in to permanent failure
	for my $unit (@service_units) {
		my $path = "${tmpdir}/usr/lib/s6-rc/sources/${unit}";
		next unless -f "$path/.permafail";
		error("$unit: .permafail and finish cannot be used together, remove one")
			if -f "$path/finish";
		my $type = slurp_first_line("$path/type");
		error("$unit: .permafail needs a longrun unit, type is '$type'")
			unless $type eq 'longrun';
		my $args = slurp_first_line("$path/.permafail");
		$args = PERMAFAIL_DEFAULT unless length $args;
		open(my $fh, '>', "$path/finish")
			or error("cannot write $path/finish: $!");
		print $fh "#!/usr/bin/execlineb -S3\n";
		print $fh "s6-permafailon $args\n";
		print $fh "exit 0\n";
		close($fh) or error("cannot write $path/finish: $!");
		doit("chmod", "0755", "$path/finish");
	}

	# remove .noenable, .norestart and .permafail hint files
	foreach my $unitdir (glob("${tmpdir}/usr/lib/s6-rc/sources/*")) {
		next unless -d $unitdir;
		doit("rm", "-f", "$unitdir/.noenable");
		doit("rm", "-f", "$unitdir/.norestart");
		doit("rm", "-f", "$unitdir/.permafail");
	}

	# quote and sort to be used in autoscript template
	@service_units = map { quote($_) } uniq sort @service_units;
	@enable_units = map { quote($_) } uniq sort @enable_units;
	@restart_units = map { quote($_) } uniq sort @restart_units;

	if (@enable_units) {
		# preinst records which units exist before unpack; postinst enables the rest
		autoscript($package, 'preinst', 'preinst-s6-record', {'UNITFILES' => join(' ', @enable_units), 'PACKAGE' => $package });
		autoscript($package, 'postinst', 'postinst-s6-enable', {'UNITFILES' => join(' ', @enable_units), 'PACKAGE' => $package });
	} else {
		autoscript($package, 'postinst', 'postinst-s6-compile');
	}

	if (@service_units) {
		autoscript($package, 'postrm', 'postrm-s6-compile', { 'PACKAGE' => $package });
		autoscript($package, 'prerm', 'prerm-s6-stop', { 'UNITFILES' => join(' ', @service_units) })
	}

	if (@restart_units) {
		autoscript($package, 'postinst', 'postinst-s6-restart', { 'UNITFILES' => join(' ', @restart_units) });
	}
}

=head1 SEE ALSO

L<debhelper(7)>

=head1 AUTHORS

info@automatic-server.com

=cut
