#!/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;