Blob Blame History Raw
#!/usr/bin/perl

# Copyright 2014 Jan Pazdziora
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#     http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

use strict;
use warnings FATAL => 'all';

use IO::File ();
use IO::Dir ();
use IO::Socket::UNIX ();
use Socket ();
use POSIX ();
use Time::HiRes qw(sleep time);

my $SYSTEMCTL_LOG = $ENV{"_SYSTEMCTL_LITE_LOGFILE"};
if (not defined $SYSTEMCTL_LOG) {
	$SYSTEMCTL_LOG = '/var/log/systemctl.log';
}
sub log_command {
	local * LOG;
	open(LOG, '>>', $SYSTEMCTL_LOG);
	print LOG @_;
	close LOG;
}
log_command("[@ARGV]\n");
shift if @ARGV and $ARGV[0] eq '-q';

for (my $i = 0; $i < @ARGV; $i++) {
	if ($ARGV[$i] eq '--ignore-dependencies') {
		splice @ARGV, $i, 1;
		last;
	}
}
if (@ARGV == 1 and $ARGV[0] eq 'daemon-reload') {
	exit 0;
}
if (@ARGV == 2 and $ARGV[0] eq '--system' and $ARGV[1] eq 'daemon-reload') {
	exit 0;
}

for (keys %ENV) {
	delete $ENV{$_} unless $_ =~ /^_SYSTEMCTL_LITE/;
}

my $RUNNING_DIR = '/run/systemctl-lite-running';
if (not -d $RUNNING_DIR) {
	mkdir $RUNNING_DIR;
}
my $ENABLED_DIR = '/etc/systemctl-lite-enabled';
if (defined $ENV{_SYSTEMCTL_LITE_ENABLED_DIR}) {
	$ENABLED_DIR = $ENV{_SYSTEMCTL_LITE_ENABLED_DIR};
}

if (@ARGV == 1) {
	if ($ARGV[0] eq 'start-enabled' and -d $ENABLED_DIR) {
		local * ENABLED;
		opendir ENABLED, $ENABLED_DIR;
		my %services;
		while (defined(my $f = readdir ENABLED)) {
			next if $f eq '.' or $f eq '..';
			my $modified = (stat "$ENABLED_DIR/$f")[9];
			if (defined $modified) {
				$services{$f} = $modified;
			}
		}
		close ENABLED;
		for my $s (sort { $services{$a} <=> $services{$b} or $a cmp $b } keys %services) {
			print "Starting [$s]\n";
			system $0, 'start', $s;
			exit ($? >> 8) if ($? >> 8);
		}
	}
	if ($ARGV[0] eq 'stop-running' and -d $RUNNING_DIR) {
		local * RUNNING;
		opendir RUNNING, $RUNNING_DIR;
		my %services;
		while (defined(my $f = readdir RUNNING)) {
			next if $f eq '.' or $f eq '..';
			my $modified = (stat "$RUNNING_DIR/$f")[9];
			if (defined $modified) {
				my $trimmed = $f;
				$trimmed =~ s/\..+$//;
				$services{$trimmed} = $modified;
			}
		}
		close RUNNING;
		for my $s (sort { $services{$b} <=> $services{$a} or $a cmp $b } keys %services) {
			system $0, 'stop', $s;
		}
	}
	exit;
}

if (@ARGV != 2) {
	die "Usage: $0 (start|stop|status|...) (service|target|socket thing)\n";
}

my ($COMMAND, $SERVICE) = @ARGV;
my $TYPE = 'service';
if ($SERVICE =~ /\.(target|socket)$/) {
	$TYPE = $1;
} elsif (not $SERVICE =~ /\.service$/) {
	$SERVICE .= '.service';
}

my @PATHS = (
	'/etc/systemd/system',
	'/run/systemd/system',
	'/usr/lib/systemd/system',
);

my $FULL_SERVICE = $SERVICE;
my $INSTANCE = undef;

my $file = undef;
if ($SERVICE =~ s/\@(.+)\.service$/\@.service/) {
	$INSTANCE = $1;
}
for my $p (@PATHS) {
	if (-e "$p/$SERVICE") {
		if (-l "$p/$SERVICE") {
			my $new_service = readlink("$p/$SERVICE");
			log_command("Service [$p/$SERVICE] is a symlink to [$new_service]\n");
			$new_service =~ s!^.*/!!;
			$SERVICE = $new_service;
		}
		$file = "$p/$SERVICE";
		last;
	}
}

if ($SERVICE =~ s/\@(.+)\.service$/\@.service/) {
	$INSTANCE = $1;
	for my $p (@PATHS) {
		if (-f "$p/$SERVICE") {
			$file = "$p/$SERVICE";
			last;
		}
	}
}

if (not defined $file) {
	if ($COMMAND eq 'is-enabled') {
		exit 1;
	}
	if ($COMMAND eq 'is-active') {
		exit 3;
	}
	warn "No service definition found for [$FULL_SERVICE].\n";
	exit 2;
}

sub parse_file {
	my ($file, $data) = @_;
	if (-d $file) {
		my $op;
		if ($file =~ /\.requires$/) {
			$op = 'Unit.Requires';
		} elsif ($file =~ /\.wants$/) {
			$op = 'Unit.Wants';
		} else {
			die "Unknown directory [$file].\n";
		}
		my $dh = new IO::Dir($file);
		while (defined(my $de = $dh->read)) {
			next if $de eq '.' or $de eq '..';
			push @{$data->{$op}}, $de unless $de =~ /\.target$/;
		}
		$dh->close;
		return;
	}
	my $fh = new IO::File($file);
	my $section = 'undefined';
	while (my $line = <$fh>) {
		chomp $line;
		if ($line =~ /^\[(.+)\]\s*$/) {
			$section = $1;
			next;
		}
		next if $line =~ /^\s*(#|$)/;
		if ($line =~ /^\.include\s(.+)/) {
			parse_file($1, $data);
			next;
		}
		my ($key, $value) = split /=/, $line, 2;
		if (defined $INSTANCE) {
			$value =~ s/\%i/$INSTANCE/g;
		}
		if ($key eq 'EnvironmentFile') {
			if ($value eq '') {
				delete $data->{"$section.$key"};
			} else {
				push @{ $data->{"$section.$key"} }, $value;
			}
		} elsif ($key =~ /^(Wants|Requires)$/) {
			push @{ $data->{"$section.$key"} }, $value unless $value =~ /\.target$/;
		} elsif ($key =~ /^(ExecStart(Pre|Post)|ExecReload|ExecStop(Pre|Post)?)$/) {
			push @{ $data->{"$section.$key"} }, $value;
		} else {
			$data->{"$section.$key"} = $value;
		}
	}
	$fh->close;
	if (defined $data->{'Service.ExecStart'} and not defined $data->{'Service.PIDFile'}) {
		my ($pidfile) = grep /^\/.+\.pid$/, split /\s+/, $data->{'Service.ExecStart'};
		if (defined $pidfile) {
			$data->{'Service.PIDFile'} = $pidfile;
			log_command("Guessing pid file [$data->{'Service.PIDFile'}] from ExecStart [$data->{'Service.ExecStart'}]\n");
		}
	}
}

my $data = {};
parse_file($file, $data);
for my $p (@PATHS) {
	if (-d "$p/$SERVICE.wants") {
		$file = "$p/$SERVICE.wants";
		parse_file("$p/$SERVICE.wants", $data);
		last;
	}
}

sub pidof {
	my $command = shift;
	my $pids = `/usr/sbin/pidof $command`;
	chomp $pids;
	if ($pids ne '') {
		return split /\s+/, $pids;
	}
	return;
}

sub get_exec_start {
	my $data = shift;
	my $d = $data->{'Service.ExecStart'};
	if (not defined $d) {
		warn "No ExecStart value found for [$SERVICE].\n";
		exit 3;
	}
	return split /\s+/, $d;
}

sub get_pid {
	my $file = shift;
	if (not $file =~ m!^/!) {
		$file = "$RUNNING_DIR/$file";
	}
	if (-f $file) {
		local * PIDFILE;
		open PIDFILE, '<', $file;
		my $pid = <PIDFILE>;
		close PIDFILE;
		if (defined $pid) {
			chomp $pid;
			return $pid;
		}
	}
}

sub is_running {
	my $data = shift;
	my $ret;
	if (-f "$RUNNING_DIR/$FULL_SERVICE.oneshot") {
		return 1;
	} elsif (defined $data->{'Service.PIDFile'}) {
		my $pid = get_pid($data->{'Service.PIDFile'});
		if (defined $pid) {
			$ret = kill 0, $pid;
		}
	} else {
		my $path = get_pid("$FULL_SERVICE.name");
		if (defined $path) {
			if (pidof($path)) {
				$ret = 1;
			}
		} else {
			my $pid = get_pid("$FULL_SERVICE.pid");
			if (defined $pid) {
				$ret = kill 0, $pid;
			}
		}
	}
	return $ret;
}
if ($COMMAND eq 'is-active' or $COMMAND eq 'status') {
	if (is_running($data)) {
		print "active\n";
		exit;
	}
	print "inactive\n";
	exit 3;
}

sub exec_stop_pre {
	my ($data) = @_;
	if (defined $data->{'Socket.ExecStopPre'}) {
		for my $x (@{ $data->{'Socket.ExecStopPre'} }) {
			log_command("Running stop pre [$x]\n");
			system $x;
		}
	}
}
sub exec_stop_post {
	my ($data) = @_;
	if (defined $data->{'Service.ExecStopPost'}) {
		for my $x (@{ $data->{'Service.ExecStopPost'} }) {
			log_command("Running stop post [$x]\n");
			system $x;
		}
	}
}

if ($TYPE eq 'target' and not defined $data->{'Unit.Wants'} and not defined $data->{'Unit.Requires'}) {
	warn "No Unit.Wants/.wants/Unit.Requires/.requires list for target [$SERVICE]\n";
	exit 8;
}

if ($COMMAND eq 'restart') {
	if ($TYPE ne 'service' or is_running($data)) {
		system $0, 'stop', $FULL_SERVICE;
	}
	my $ret = system $0, 'start', $FULL_SERVICE;
	exit $ret >> 8;
}

sub pids_went_away_timeout {
	my $timeout = shift;
	my $start = time;
	while (kill 0, @_) {
		if (time - $start > $timeout) {
			log_command(sprintf(" ** pid(s) [%s] not killed within %d s\n", join(', ', @_), $timeout));
			return 0;
		}
		sleep 0.1;
	}
	log_command(sprintf(" ** pid(s) [%s] got killed after %.2f s\n", join(', ', @_), time - $start));
	return 1;
}

sub stop_pids {
	my @pids = @_;
	kill 15, @pids;
	if (not pids_went_away_timeout(5, @pids)) {
		kill 9, @pids;
		if (not pids_went_away_timeout(5, @pids)) {
			log_command("Failed to kill [@pids] even with 9\n");
			warn "Failed to kill [@pids].\n";
			return 1;
		}
		log_command("Killed [@pids] with 9\n");
	} else {
		log_command("Killed [@pids] with 15\n");
	}
	return 0;
}

if ($COMMAND eq 'stop') {
	my $mainpid;
	if (defined $data->{'Service.PIDFile'}) {
		$mainpid = get_pid($data->{'Service.PIDFile'});
	} else {
		$mainpid = get_pid("$FULL_SERVICE.pid");
	}
	if (defined $data->{'Service.ExecStop'}) {
		for my $x (@{ $data->{'Service.ExecStop'} }) {
			my $runit = 1;
			if (defined $mainpid) {
				$x =~ s!\$MAINPID\b|\$\{MAINPID\}!$mainpid!g;
			} elsif ($x =~ /\$MAINPID\b|\$\{MAINPID\}/) {
				$runit = 0;
				log_command("Service [$FULL_SERVICE] would like to stop via ExecStop [$x] but we have no pid file, skipping.\n");
			}
			if ($runit) {
				log_command("Running stop [$x]\n");
				system $x;
				sleep 1;
			}
		}
	}
	my $ret = undef;
	if ($TYPE eq 'target') {
		# noop
	} elsif (defined $data->{'Service.Type'} and $data->{'Service.Type'} eq 'oneshot') {
		# noop
	} elsif ($TYPE eq 'socket') {
		my $pids = `/usr/sbin/fuser $data->{'Socket.ListenStream'} 2> /dev/null`;
		if (defined $pids) {
			chomp $pids;
			$pids =~ s/^\s+//;
			my @pids = split /\s+/, $pids;
			if (@pids) {
				exec_stop_pre($data);
				log_command("Will kill [@pids] as fuser [$data->{'Socket.ListenStream'}] of [$FULL_SERVICE]\n");
				$ret = stop_pids(@pids);
			}
		}
	} elsif (defined $data->{'Service.PIDFile'}) {
		my $pid = get_pid($data->{'Service.PIDFile'});
		if (defined $pid) {
			log_command("Will kill [$pid] found in Service.PIDFile of [$FULL_SERVICE]\n");
			$ret = stop_pids($pid);
		}
	} else {
		my $path = get_pid("$FULL_SERVICE.name");
		if (defined $path) {
			if (my @pids = pidof($path)) {
				log_command("Will kill [@pids] as pidof [$path] found in [$FULL_SERVICE.name]\n");
				$ret = stop_pids(@pids);
			} else {
				warn "No pidof for [$path] found in [$FULL_SERVICE.name].\n";
			}
		} else {
			my $pid = get_pid("$FULL_SERVICE.pid");
			if (defined $pid) {
				log_command("Will kill [$pid] found in [$FULL_SERVICE.pid]\n");
				$ret = stop_pids($pid);
			} else {
				warn "No pid and no name for [$FULL_SERVICE].\n";
			}
		}
	}
	exec_stop_post($data);
	unlink "$RUNNING_DIR/$FULL_SERVICE.pid", "$RUNNING_DIR/$FULL_SERVICE.name", "$RUNNING_DIR/$FULL_SERVICE.oneshot";
	if (defined $data->{'Unit.Wants'}) {
		for my $x (@{ $data->{'Unit.Wants'} }) {
			system $0, 'stop', $x;
		}
	}
	if (defined $data->{'Unit.Requires'}) {
		for my $x (@{ $data->{'Unit.Requires'} }) {
			system $0, 'stop', $x;
		}
	}
	exit ( defined $ret ? $ret : 0 );
}

sub add_runuser {
	my ($data, $cmd) = @_;
	if (defined $data->{'Service.User'}) {
		unshift @$cmd, '-u', $data->{'Service.User'}, '--';
		if (defined $data->{'Service.Group'}) {
			unshift @$cmd, '-g', $data->{'Service.Group'};
		}
		unshift @$cmd, '/usr/sbin/runuser';
	}
}

if ($COMMAND eq 'start') {
	my (@starting_stack, %starting_stack);
	if (defined $ENV{_SYSTEMCTL_LITE_STARTING}) {
		@starting_stack = split /:/, $ENV{_SYSTEMCTL_LITE_STARTING};
		@starting_stack{@starting_stack} = ();
		$ENV{_SYSTEMCTL_LITE_STARTING} .= ":$FULL_SERVICE";
	} else {
		$ENV{_SYSTEMCTL_LITE_STARTING} = $FULL_SERVICE;
	}

	if ($TYPE eq 'service' and is_running($data)) {
		log_command("Service [$FULL_SERVICE] already found running, not starting again.\n");
		exit;
	}
	if (defined $data->{'Service.PIDFile'}) {
		log_command("Service [$FULL_SERVICE] defines PIDFile [$data->{'Service.PIDFile'}], unlinking it before start\n");
		unlink $data->{'Service.PIDFile'};
	}
	if (defined $data->{'Unit.Wants'}) {
		for my $x (@{ $data->{'Unit.Wants'} }) {
			if (exists $starting_stack{$x}) {
				log_command("Skipping start of [$x], we are already in the process of starting it.\n");
				next;
			}
			my @cmd = ($0, 'start', $x);
			log_command("Running [@cmd] for Unit.Wants of [$FULL_SERVICE]\n");
			system @cmd;
		}
	}
	if (defined $data->{'Unit.Requires'}) {
		for my $x (@{ $data->{'Unit.Requires'} }) {
			if (exists $starting_stack{$x}) {
				log_command("Skipping start of [$x], we are already in the process of starting it.\n");
				next;
			}
			my @cmd = ($0, 'start', $x);
			log_command("Running [@cmd] for Unit.Requires of [$FULL_SERVICE]\n");
			if (system @cmd) {
				log_command("Failed to start [$x], aborting start\n");
				exit 1;
			}
		}
	}
	if (defined $data->{'Service.PIDFile'} and is_running($data)) {
		log_command("Service [$FULL_SERVICE] defines PIDFile [$data->{'Service.PIDFile'}] and it seems to have already started, not starting again.\n");
		exit;
	}

	if ($TYPE eq 'target') {
		exit;
	}
	if ($TYPE eq 'socket' and -S $data->{'Socket.ListenStream'}) {
		my $out = `/usr/sbin/fuser $data->{'Socket.ListenStream'} 2> /dev/null`;
		if (defined $out and $out ne '') {
			log_command("Service [$FULL_SERVICE] already found active on socket [$data->{'Socket.ListenStream'}], not starting again.\n");
			exit;
		}
	}
	if (defined $data->{'Service.Type'} and $data->{'Service.Type'} eq 'dbus') {
		my @cmd = ($0, 'start', 'dbus.socket');
		log_command("Running [@cmd] for Service.Type dbus\n");
		system @cmd;
	}
	if (defined $data->{'Service.ExecStartPre'}) {
		for my $x (@{ $data->{'Service.ExecStartPre'} }) {
			my $can_fail = 0;
			if ($x =~ s/^-//) {
				$can_fail = 1;
			}
			my @cmd = split /\s+/, $x;
			if (not $data->{'Service.PermissionsStartOnly'} or $data->{'Service.PermissionsStartOnly'} =~ /^(false|0)$/i) {
				add_runuser($data, \@cmd);
			}
			no warnings 'uninitialized';
			log_command("Running start pre [@cmd]\n");
			if (system @cmd and not $can_fail) {
				exit 1;
			}
		}
	}
	my @paths;
	if ($TYPE eq 'socket') {
		my $service = $SERVICE;
		if (defined $data->{'Socket.Accept'} and $data->{'Socket.Accept'} eq 'true') {
			$service =~ s/\.socket$/\@.service/;
			@paths = ( '/bin/systemctl-socket-daemon', $data->{'Socket.ListenStream'}, $data->{'Socket.SocketMode'} // '0666', $service );
		} else {
			$service =~ s/\.socket$/\.service/;
			my $ret = system "$0 is-active $service > /dev/null";
			if (($ret >> 8) == 0) {
				log_command("Service [$service] is already running for [$SERVICE]\n");
				exit;
			}
			@paths = ( $0, 'start', $service );
		}
	} else {
		@paths = get_exec_start($data);
	}
	my $first_path = $paths[0];

	my $ENV = '';
	if (exists $data->{'Service.EnvironmentFile'}) {
		for my $e (@{ $data->{'Service.EnvironmentFile'} }) {
			my $error_fail = 1;
			if ($e =~ s/^-//) {
				$error_fail = 0;
			}
			my $fh = new IO::File($e);
			if (not defined $fh) {
				if ($error_fail) {
					warn "Error reading EnvironmentFile [$e]: $!\n";
					exit 5;
				}
			} else {
				while (my $line = <$fh>) {
					chomp $line;
					next if $line =~ /^\s*(#|$)/;
					$ENV .= "export $line; ";
				}
				$fh->close();
			}
		}
	}
	if (defined $data->{'Service.Environment'}) {
		my $x = $data->{'Service.Environment'};
		$x =~ s/^"(.*)"$/$1/;
		$ENV .= "export $x; ";
	}
	add_runuser($data, \@paths);
	log_command("Running [$ENV@paths]\n");
	if (defined $data->{'Service.Type'} and $data->{'Service.Type'} eq 'oneshot') {
		system "$ENV@paths";
		if (defined $data->{'Service.RemainAfterExit'} and $data->{'Service.RemainAfterExit'} eq 'yes') {
			local * PIDFILE;
			open PIDFILE, '>', "$RUNNING_DIR/$FULL_SERVICE.oneshot";
			close PIDFILE;
		}
		exit;
	}
	my $pid = fork();
	die "Failed to fork for [@_]\n" if not defined $pid;
	if ($pid == 0) {
		open(STDOUT, '>>', $SYSTEMCTL_LOG);
		open(STDERR, '>>', $SYSTEMCTL_LOG);
		open(STDIN, '<', '/dev/null');
		POSIX::setsid();
		exec "$ENV@paths";
	}
	if (defined $data->{'Service.PIDFile'}) {
		log_command("Service [$FULL_SERVICE] defines PIDFile [$data->{'Service.PIDFile'}], not marking pid\n");
		unlink "$RUNNING_DIR/$FULL_SERVICE.pid";
		my $i = 0;
		while (not -f $data->{'Service.PIDFile'}) {
			sleep 0.2;
			$i++;
			if ($i > 250) {
				log_command("Starting [$FULL_SERVICE] did not create PIDFile [$data->{'Service.PIDFile'}]\n");
				exit 10;
			}
		}
		my $pid = get_pid($data->{'Service.PIDFile'});
		if (not defined $pid or not kill 0, $pid) {
			log_command("Starting [$FULL_SERVICE] created PIDFile [$data->{'Service.PIDFile'}] but the process is not running\n");
			exit 11;
		}
	} else {
		local * PIDFILE;
		open PIDFILE, '>', "$RUNNING_DIR/$FULL_SERVICE.pid";
		print PIDFILE "$pid\n";
		close PIDFILE;
		log_command("Marked pid [$pid] for [$FULL_SERVICE]\n");
		sleep 1;
		if (defined $data->{'Service.Type'} and $data->{'Service.Type'} ne 'simple' and $data->{'Service.Type'} ne 'notify' and pidof($first_path)) {
			local * PIDFILE;
			open PIDFILE, '>', "$RUNNING_DIR/$FULL_SERVICE.name";
			print PIDFILE "$first_path\n";
			close PIDFILE;
			log_command("Marked process name [$first_path] for [$FULL_SERVICE]\n");
		}
	}
	if (defined $data->{'Service.Type'}
		and $data->{'Service.Type'} eq 'dbus'
		and defined $data->{'Service.BusName'}) {
		my $busname = $data->{'Service.BusName'};
		my $objectpath = $busname;
		$objectpath =~ s!^|\.!/!g;
		for (0 .. 10) {
			system "/usr/bin/dbus-send --system --type=method_call --print-reply --dest=$busname $objectpath org.freedesktop.DBus.Introspectable.Introspect > /dev/null";
			exit if ($? >> 8) == 0;
			sleep 1;
		}
		exit 9;
	}
	exit;
}

if ($COMMAND eq 'is-enabled') {
	if (-e "$ENABLED_DIR/$FULL_SERVICE") {
		print "enabled\n";
		exit 0;
	}
	print "disabled\n";
	exit 1;
}

if ($COMMAND eq 'enable') {
	if (not -d $ENABLED_DIR) {
		mkdir $ENABLED_DIR;
	}
	local * FILE;
	open FILE, '>', "$ENABLED_DIR/$FULL_SERVICE" or die "Error creating $ENABLED_DIR/$FULL_SERVICE: $!\n";
	exit;
}

if ($COMMAND eq 'disable') {
	unlink "$ENABLED_DIR/$FULL_SERVICE" or die "Error removing $ENABLED_DIR/$FULL_SERVICE: $!\n";
	exit;
}

die "Unknown command [$COMMAND].\n";

1;