mirror of
https://github.com/webmin/webmin.git
synced 2026-06-05 04:40:24 +01:00
This PR expands the "Bootup and Shutdown" module’s systemd support with creation and management for services, timers, sockets, paths and targets. It also adds user-scoped systemd units, linger controls, status and log actions, tabbed unit listings by type, and contextual help for the new options. For user-scoped units, the implementation includes several safety guards because unit files live under user-controlled home directories: - User accounts are validated with system account data before any user-unit operation is attempted. - User unit names are restricted to known systemd unit suffixes and safe filename characters. - User unit files are limited to direct children of `~/.config/systemd/user`. - Symlinked `.config`, `.config/systemd`, and `.config/systemd/user` paths are rejected. - User unit reads, writes, directory creation, and deletes are performed after dropping privileges to the target Unix user. - File operations re-check paths close to the actual read/write/delete operation to reduce symlink race exposure. - User unit create failures roll back half-created files when daemon reload fails. - User-provided unit names, owners, paths, command output, and logs are HTML-escaped before display. - systemctl, journalctl, and loginctl command arguments are shell-quoted before execution. - User services omit `User=` and `Group=` directives because they already run under the selected user’s systemd manager. Together, all these changes will allow Webmin admin to manage both system and user systemd units while keeping user-controlled home-directory paths from becoming root-level file read/write/delete exploits. Implemented in response to these two issue requests https://github.com/webmin/webmin/issues/2733 and https://github.com/webmin/webmin/issues/2734
4043 lines
104 KiB
Perl
4043 lines
104 KiB
Perl
=head1 init-lib.pl
|
|
|
|
Common functions for SYSV-style boot/shutdown sequences, MacOS, FreeBSD
|
|
and Windows. Because each system uses a different format and semantics for
|
|
bootup actions, there are separate functions for listing and managing each
|
|
type. However, some functions like enable_at_boot and disable_at_boot can
|
|
creation actions regardless of the underlying boot system.
|
|
|
|
Example code :
|
|
|
|
foreign_require('init', 'init-lib.pl');
|
|
$ok = init::action_status('foo');
|
|
if ($ok == 0) {
|
|
init::enable_at_boot('foo', 'Start or stop the Foo server',
|
|
'/etc/foo/start', '/etc/foo/stop');
|
|
}
|
|
|
|
=cut
|
|
|
|
BEGIN { push(@INC, ".."); };
|
|
use WebminCore;
|
|
&init_config();
|
|
@action_buttons = ( 'start', 'restart', 'condrestart', 'reload', 'status',
|
|
'stop' );
|
|
%access = &get_module_acl();
|
|
|
|
=head2 init_mode
|
|
|
|
This variable is set based on the bootup system in use. Possible values are :
|
|
|
|
=item osx - MacOSX hostconfig files, for older versions
|
|
|
|
=item launchd - MacOS Launchd, for newer versions
|
|
|
|
=item rc - FreeBSD 6+ RC files
|
|
|
|
=item init - System V init.d files, seen on Linux and Solaris
|
|
|
|
=item my - A single rc.my file
|
|
|
|
=item win32 - Windows services
|
|
|
|
=item upstart - Upstart, seen on Ubuntu 11
|
|
|
|
=item systemd - SystemD, seen on Fedora 16
|
|
|
|
=cut
|
|
if ($config{'init_mode'}) {
|
|
$init_mode = $config{'init_mode'};
|
|
}
|
|
elsif (&has_command("launchd")) {
|
|
$init_mode = "launchd";
|
|
}
|
|
elsif ($config{'hostconfig'}) {
|
|
$init_mode = "osx";
|
|
}
|
|
elsif ($config{'rc_dir'}) {
|
|
$init_mode = "rc";
|
|
}
|
|
elsif ($config{'init_base'} && -d "/etc/init" &&
|
|
&has_command("initctl") &&
|
|
&execute_command("/sbin/init --version") == 0) {
|
|
$init_mode = "upstart";
|
|
}
|
|
elsif (-d "/etc/systemd" && &has_command("systemctl") &&
|
|
&execute_command("systemctl list-units") == 0) {
|
|
$init_mode = "systemd";
|
|
}
|
|
elsif ($config{'init_base'}) {
|
|
$init_mode = "init";
|
|
}
|
|
elsif ($config{'local_script'}) {
|
|
$init_mode = "local";
|
|
}
|
|
elsif ($gconfig{'os_type'} eq 'windows') {
|
|
$init_mode = "win32";
|
|
}
|
|
|
|
# Do init scripts support start and stop custom messages?
|
|
if ($init_mode eq "init" && $gconfig{'os_type'} =~ /^(osf1|hpux)$/) {
|
|
$supports_start_stop_msg = 1;
|
|
}
|
|
else {
|
|
$supports_start_stop_msg = 0;
|
|
}
|
|
|
|
# Use the chkconfig command to enable actions?
|
|
$use_chkconfig = &has_command("chkconfig") &&
|
|
$gconfig{'os_type'} ne 'syno-linux';
|
|
|
|
=head2 runlevel_actions(level, S|K)
|
|
|
|
Return a list of init.d actions started or stopped in some run-level, each of
|
|
which is a space-separated string in the format : number name inode
|
|
|
|
=cut
|
|
sub runlevel_actions
|
|
{
|
|
local($dir, $f, @stbuf, @rv);
|
|
$dir = &runlevel_dir($_[0]);
|
|
opendir(DIR, $dir);
|
|
foreach $f (readdir(DIR)) {
|
|
if ($f !~ /^([A-Z])(\d+)(.*)$/ || $1 ne $_[1]) { next; }
|
|
if (!(@stbuf = stat("$dir/$f"))) { next; }
|
|
push(@rv, "$2 $3 $stbuf[1]");
|
|
}
|
|
closedir(DIR);
|
|
@rv = sort { @a = split(/\s/,$a); @b = split(/\s/,$b); $a[0] <=> $b[0]; } @rv;
|
|
return $_[1] eq "S" ? @rv : reverse(@rv);
|
|
}
|
|
|
|
|
|
=head2 list_runlevels
|
|
|
|
Returns a list of known runlevels, such as : 2 3 5.
|
|
|
|
=cut
|
|
sub list_runlevels
|
|
{
|
|
local(@rv);
|
|
opendir(DIR, $config{init_base});
|
|
foreach (readdir(DIR)) {
|
|
if (/^rc([A-z0-9])\.d$/ || /^(boot)\.d$/) {
|
|
push(@rv, $1);
|
|
}
|
|
}
|
|
closedir(DIR);
|
|
return sort(@rv);
|
|
}
|
|
|
|
|
|
=head2 list_actions
|
|
|
|
List boot time action names from init.d, such as httpd and cron.
|
|
|
|
=cut
|
|
sub list_actions
|
|
{
|
|
local($dir, $f, @stbuf, @rv);
|
|
$dir = $config{init_dir};
|
|
opendir(DIR, $dir);
|
|
foreach $f (sort { lc($a) cmp lc($b) } readdir(DIR)) {
|
|
if ($f eq "." || $f eq ".." || $f =~ /\.bak$/ || $f eq "functions" ||
|
|
$f eq "core" || $f eq "README" || $f eq "rc" || $f eq "rcS" ||
|
|
-d "$dir/$f" || $f =~ /\.swp$/ || $f eq "skeleton" ||
|
|
$f =~ /\.lock$/ || $f =~ /\.dpkg-(old|dist)$/ ||
|
|
$f =~ /^\.depend\./ || $f eq '.legacy-bootordering' ||
|
|
$f =~ /^mandrake/) { next; }
|
|
if (@stbuf = stat("$dir/$f")) {
|
|
push(@rv, "$f $stbuf[1]");
|
|
}
|
|
}
|
|
closedir(DIR);
|
|
foreach $f (split(/\s+/, $config{'extra_init'})) {
|
|
if (@stbuf = stat($f)) {
|
|
push(@rv, "$f $stbuf[1]");
|
|
}
|
|
}
|
|
return @rv;
|
|
}
|
|
|
|
|
|
=head2 action_levels(S|K, action)
|
|
|
|
Return a list of run levels in which some action (from init.d) is started
|
|
or stopped. Each item is a space-separated string in the format : level order name
|
|
|
|
=cut
|
|
sub action_levels
|
|
{
|
|
local(@stbuf, $rl, $dir, $f, @stbuf2, @rv);
|
|
@stbuf = stat(&action_filename($_[1]));
|
|
foreach $rl (&list_runlevels()) {
|
|
$dir = &runlevel_dir($rl);
|
|
opendir(DIR, $dir);
|
|
foreach $f (readdir(DIR)) {
|
|
if ($f =~ /^([A-Z])(\d+)(.*)$/ && $1 eq $_[0]) {
|
|
@stbuf2 = stat("$dir/$f");
|
|
if ($stbuf[1] == $stbuf2[1]) {
|
|
push(@rv, "$rl $2 $3");
|
|
last;
|
|
}
|
|
}
|
|
}
|
|
closedir(DIR);
|
|
}
|
|
return @rv;
|
|
}
|
|
|
|
|
|
=head2 action_filename(name)
|
|
|
|
Returns the path to the file in init.d for some action, such as /etc/init.d/foo.
|
|
|
|
=cut
|
|
sub action_filename
|
|
{
|
|
return $_[0] =~ /^\// ? $_[0] : "$config{init_dir}/$_[0]";
|
|
}
|
|
|
|
=head2 action_unit(name)
|
|
|
|
Returns a full systemd unit name, defaulting bare legacy action names to
|
|
.service units.
|
|
|
|
=cut
|
|
sub action_unit
|
|
{
|
|
my ($unit) = @_;
|
|
my $units_piped = join('|', &get_systemd_unit_types());
|
|
$unit .= ".service"
|
|
if ($unit !~ /\.($units_piped)$/);
|
|
return $unit;
|
|
}
|
|
|
|
=head2 runlevel_filename(level, S|K, order, name)
|
|
|
|
Returns the path to the actual script run at boot for some action, such as
|
|
/etc/rc3.d/S99foo.
|
|
|
|
=cut
|
|
sub runlevel_filename
|
|
{
|
|
my $n = $_[3];
|
|
$n =~ s/^(.*)\///;
|
|
return &runlevel_dir($_[0])."/$_[1]$_[2]$n";
|
|
}
|
|
|
|
|
|
=head2 add_rl_action(action, runlevel, S|K, order)
|
|
|
|
Add some existing action to a runlevel. The parameters are :
|
|
|
|
=item action - Name of the action, like foo
|
|
|
|
=item runlevel - A runlevel number, like 3
|
|
|
|
=item S|K - Either S for an action to run at boot, or K for shutdown
|
|
|
|
=item order - Numeric boot order, like 99
|
|
|
|
=cut
|
|
sub add_rl_action
|
|
{
|
|
$file = &runlevel_filename($_[1], $_[2], $_[3], $_[0]);
|
|
while(-r $file) {
|
|
if ($file =~ /^(.*)_(\d+)$/) { $file = "$1_".($2+1); }
|
|
else { $file = $file."_1"; }
|
|
}
|
|
&lock_file($file);
|
|
&symlink_file(&action_filename($_[0]), $file);
|
|
&unlock_file($file);
|
|
}
|
|
|
|
|
|
=head2 delete_rl_action(name, runlevel, S|K)
|
|
|
|
Delete some action from a runlevel. The parameters are :
|
|
|
|
=item action - Name of the action, like foo.
|
|
|
|
=item runlevel - A runlevel number, like 3.
|
|
|
|
=item S|K - Either S for an action to run at boot, or K for shutdown.
|
|
|
|
=cut
|
|
sub delete_rl_action
|
|
{
|
|
local(@stbuf, $dir, $f, @stbuf2);
|
|
@stbuf = stat(&action_filename($_[0]));
|
|
$dir = &runlevel_dir($_[1]);
|
|
opendir(DIR, $dir);
|
|
foreach $f (readdir(DIR)) {
|
|
if ($f =~ /^([A-Z])(\d+)(.+)$/ && $1 eq $_[2]) {
|
|
@stbuf2 = stat("$dir/$f");
|
|
if ($stbuf[1] == $stbuf2[1]) {
|
|
# found file to delete.. unlink
|
|
&unlink_logged("$dir/$f");
|
|
last;
|
|
}
|
|
}
|
|
}
|
|
closedir(DIR);
|
|
}
|
|
|
|
|
|
=head2 reorder_rl_action(name, runlevel, S|K, new_order)
|
|
|
|
Change the boot order of some existing runlevel action. The parameters are :
|
|
|
|
=item action - Name of the action, like foo.
|
|
|
|
=item runlevel - A runlevel number, like 3.
|
|
|
|
=item S|K - Either S for an action to run at boot, or K for shutdown.
|
|
|
|
=item new_order - New numeric boot order to use, like 99.
|
|
|
|
=cut
|
|
sub reorder_rl_action
|
|
{
|
|
local(@stbuf, $dir, $f, @stbuf2);
|
|
@stbuf = stat(&action_filename($_[0]));
|
|
$dir = &runlevel_dir($_[1]);
|
|
opendir(DIR, $dir);
|
|
foreach $f (readdir(DIR)) {
|
|
if ($f =~ /^([A-Z])(\d+)(.+)$/ && $1 eq $_[2]) {
|
|
@stbuf2 = stat("$dir/$f");
|
|
if ($stbuf[1] == $stbuf2[1]) {
|
|
# Found file that needs renaming
|
|
$file = &runlevel_dir($_[1])."/$1$_[3]$3";
|
|
while(-r $file) {
|
|
if ($file =~ /^(.*)_(\d+)$/)
|
|
{ $file = "$1_".($2+1); }
|
|
else { $file = $file."_1"; }
|
|
}
|
|
&rename_logged("$dir/$f", $file);
|
|
last;
|
|
}
|
|
}
|
|
}
|
|
closedir(DIR);
|
|
}
|
|
|
|
|
|
=head2 rename_action(old, new)
|
|
|
|
Change the name of an action in init.d, and re-direct all soft links
|
|
to it from the runlevel directories. Parameters are :
|
|
|
|
=item old - Old action name.
|
|
|
|
=item new - New action name.
|
|
|
|
=cut
|
|
sub rename_action
|
|
{
|
|
local($file, $idx, $old);
|
|
foreach (&action_levels('S', $_[0])) {
|
|
/^(\S+)\s+(\S+)\s+(\S+)$/;
|
|
$file = &runlevel_dir($1)."/S$2$3";
|
|
if (readlink($file)) {
|
|
# File is a symbolic link.. change it
|
|
&lock_file($file);
|
|
&unlink_file($file);
|
|
&symlink_file("$config{init_dir}/$_[1]", $file);
|
|
&unlock_file($file);
|
|
}
|
|
if (($idx = index($file, $_[0])) != -1) {
|
|
$old = $file;
|
|
substr($file, $idx, length($_[0])) = $_[1];
|
|
&rename_logged($old, $file);
|
|
}
|
|
}
|
|
foreach (&action_levels('K', $_[0])) {
|
|
/^(\S+)\s+(\S+)\s+(\S+)$/;
|
|
$file = &runlevel_dir($1)."/K$2$3";
|
|
if (readlink($file)) {
|
|
# File is a symbolic link.. change it
|
|
&lock_file($file);
|
|
&unlink_file($file);
|
|
&symlink_file("$config{init_dir}/$_[1]", $file);
|
|
&unlock_file($file);
|
|
}
|
|
if (($idx = index($file, $_[0])) != -1) {
|
|
$old = $file;
|
|
substr($file, $idx, length($_[0])) = $_[1];
|
|
&rename_logged($old, $file);
|
|
}
|
|
}
|
|
&rename_logged("$config{init_dir}/$_[0]", "$config{init_dir}/$_[1]");
|
|
}
|
|
|
|
|
|
=head2 rename_rl_action(runlevel, S|K, order, old, new)
|
|
|
|
Change the name of a runlevel file. For internal use only.
|
|
|
|
=cut
|
|
sub rename_rl_action
|
|
{
|
|
&rename_logged(&runlevel_dir($_[0])."/$_[1]$_[2]$_[3]",
|
|
&runlevel_dir($_[0])."/$_[1]$_[2]$_[4]");
|
|
}
|
|
|
|
=head2 get_inittab_runlevel
|
|
|
|
Returns the runlevels entered at boot time. If more than one is returned,
|
|
actions from all of them are used.
|
|
|
|
=cut
|
|
sub get_inittab_runlevel
|
|
{
|
|
my %iconfig = &foreign_config("inittab");
|
|
my @rv;
|
|
my $id = $config{'inittab_id'};
|
|
if (open(TAB, "<".$iconfig{'inittab_file'})) {
|
|
# Read the inittab file
|
|
while(<TAB>) {
|
|
if (/^$id:(\d+):/ && $1) { @rv = ( $1 ); }
|
|
}
|
|
close(TAB);
|
|
}
|
|
|
|
if (&has_command("runlevel")) {
|
|
# Use runlevel command to get current level
|
|
my $out = &backquote_command("runlevel");
|
|
if ($out =~ /^(\S+)\s+(\S+)/) {
|
|
push(@rv, $2);
|
|
}
|
|
}
|
|
elsif (&has_command("who")) {
|
|
# Use who -r command to get runlevel
|
|
my $out = &backquote_command("who -r 2>/dev/null");
|
|
if (!$? && $out =~ /run-level\s+(\d+)/) {
|
|
push(@rv, $1);
|
|
}
|
|
}
|
|
|
|
# Last ditch fallback - assume runlevel 3
|
|
if (!@rv && !$config{'inittab_extra'}) {
|
|
push(@rv, 3);
|
|
}
|
|
|
|
# Add statically configured runlevels
|
|
if (@rv && $config{"inittab_rl_$rv[0]"}) {
|
|
@rv = split(/,/, $config{"inittab_rl_$rv[0]"});
|
|
}
|
|
push(@rv, $config{'inittab_extra'});
|
|
return &unique(@rv);
|
|
}
|
|
|
|
=head2 init_description(file, [&hasargs])
|
|
|
|
Given a full path to an init.d file, returns a description from the comments
|
|
about what it does. If the hasargs hash ref parameter is given, it is filled
|
|
in with supported parameters to the action, like 'start' and 'stop'.
|
|
|
|
=cut
|
|
sub init_description
|
|
{
|
|
# Read contents of script, extract start/stop commands
|
|
open(FILE, "<".$_[0]);
|
|
my @lines = <FILE>;
|
|
close(FILE);
|
|
my $data = join("", @lines);
|
|
if ($_[1]) {
|
|
foreach (@lines) {
|
|
if (/^\s*(['"]?)([a-z]+)\1\)/i) {
|
|
$_[1]->{$2}++;
|
|
}
|
|
}
|
|
}
|
|
|
|
my $desc;
|
|
if ($config{'chkconfig'}) {
|
|
# Find the redhat-style description: section
|
|
foreach (@lines) {
|
|
s/\r|\n//g;
|
|
if (/^#+\s*description:(.*?)(\\?$)/) {
|
|
$desc = $1;
|
|
}
|
|
elsif (/^#+\s*(.*?)(\\?$)/ && $desc && $1) {
|
|
$desc .= "\n".$1;
|
|
}
|
|
if ($desc && !$2) {
|
|
last;
|
|
}
|
|
}
|
|
}
|
|
elsif ($config{'init_info'} || $data =~ /BEGIN INIT INFO/) {
|
|
# Find the suse-style Description: line
|
|
foreach (@lines) {
|
|
s/\r|\n//g;
|
|
if (/^#\s*(Description|Short-Description):\s*(.*)/) {
|
|
$desc = $2;
|
|
}
|
|
}
|
|
}
|
|
else {
|
|
# Use the first comments
|
|
foreach (@lines) {
|
|
s/\r|\n//g;
|
|
next if (/^#!\s*\/(bin|sbin|usr)\// || /\$id/i || /^#+\s+@/ ||
|
|
/source function library/i || /^#+\s*copyright/i);
|
|
if (/^#+\s*(.*)/) {
|
|
last if ($desc && !$1);
|
|
$desc .= $1."\n" if ($1);
|
|
}
|
|
elsif (/\S/) { last; }
|
|
}
|
|
$_[0] =~ /\/([^\/]+)$/;
|
|
$desc =~ s/^Tag\s+(\S+)\s*//i;
|
|
$desc =~ s/^\s*$1\s+//;
|
|
}
|
|
return $desc;
|
|
}
|
|
|
|
=head2 chkconfig_info(file)
|
|
|
|
If a file has a chkconfig: section specifying the runlevels to start in and
|
|
the orders to use, return an array containing the levels (as array ref),
|
|
start order, stop order and description.
|
|
|
|
=cut
|
|
sub chkconfig_info
|
|
{
|
|
my @rv;
|
|
my $desc;
|
|
open(FILE, "<".$_[0]);
|
|
while(<FILE>) {
|
|
if (/^#\s*chkconfig:\s+(\S+)\s+(\d+)\s+(\d+)/) {
|
|
@rv = ( $1 eq '-' ? [ ] : [ split(//, $1) ], $2, $3 );
|
|
}
|
|
elsif (/^#\s*description:\s*(.*)/) {
|
|
$desc = $1;
|
|
}
|
|
}
|
|
close(FILE);
|
|
$rv[3] = $desc if ($desc && @rv);
|
|
return @rv;
|
|
}
|
|
|
|
=head2 action_status(action)
|
|
|
|
Returns 0 if some action doesn't exist, 1 if it does but is not enabled,
|
|
or 2 if it exists and is enabled. This works for all supported boot systems,
|
|
such as init.d, OSX and FreeBSD.
|
|
|
|
=cut
|
|
sub action_status
|
|
{
|
|
my ($name) = @_;
|
|
if ($init_mode eq "upstart") {
|
|
# Check upstart service status
|
|
my $out = &backquote_command("initctl status ".
|
|
quotemeta($name)." 2>&1");
|
|
if (!$?) {
|
|
my $cfile = "/etc/init/$name.conf";
|
|
open(CONF, "<".$cfile);
|
|
while(<CONF>) {
|
|
if (/^(#*)\s*start/) {
|
|
return $1 ? 1 : 2;
|
|
}
|
|
}
|
|
close(CONF);
|
|
return 1; # Should never happen
|
|
}
|
|
}
|
|
elsif ($init_mode eq "systemd") {
|
|
# Check systemd service status
|
|
my $unit = &action_unit($name);
|
|
my $out = &backquote_command("systemctl show ".
|
|
quotemeta($unit)." 2>&1");
|
|
if ($out =~ /UnitFileState=(\S+)/ &&
|
|
$out !~ /Description=LSB:\s/) {
|
|
# Exists .. but is it started at boot?
|
|
return lc($1) eq 'enabled' || lc($1) eq 'static' ? 2 : 1;
|
|
}
|
|
else {
|
|
my $out = &backquote_command("systemctl is-enabled ".
|
|
quotemeta($unit)." 2>&1");
|
|
$out = &trim($out);
|
|
return 2 if ($out eq "enabled");
|
|
return 1 if ($out eq "disabled");
|
|
}
|
|
}
|
|
if ($init_mode eq "init" || $init_mode eq "upstart" ||
|
|
$init_mode eq "systemd") {
|
|
# Look for init script
|
|
my ($a, $exists, $starting, %daemon);
|
|
foreach $a (&list_actions()) {
|
|
my @a = split(/\s+/, $a);
|
|
if ($a[0] eq $name) {
|
|
$exists++;
|
|
my @boot = &get_inittab_runlevel();
|
|
foreach $s (&action_levels("S", $a[0])) {
|
|
my ($l, $p) = split(/\s+/, $s);
|
|
$starting++ if (&indexof($l, @boot) >= 0);
|
|
}
|
|
}
|
|
}
|
|
return !$exists ? 0 : $starting ? 2 : 1;
|
|
}
|
|
elsif ($init_mode eq "local") {
|
|
# Look for entry in rc.local
|
|
my $fn = "$module_config_directory/$name.sh";
|
|
my $cmd = "$fn start";
|
|
open(LOCAL, "<".$config{'local_script'});
|
|
while(<LOCAL>) {
|
|
s/\r|\n//g;
|
|
$found++ if ($_ eq $cmd);
|
|
}
|
|
close(LOCAL);
|
|
return $found && -r $fn ? 2 : -r $fn ? 1 : 0;
|
|
}
|
|
elsif ($init_mode eq "win32") {
|
|
# Look for a win32 service, enabled at boot
|
|
my ($svc) = &list_win32_services($name);
|
|
return !$svc ? 0 :
|
|
$svc->{'boot'} == 2 ? 2 : 1;
|
|
}
|
|
elsif ($init_mode eq "rc") {
|
|
# Look for an RC script
|
|
my @rcs = &list_rc_scripts();
|
|
my ($rc) = grep { $_->{'name'} eq $name } @rcs;
|
|
return !$rc ? 0 :
|
|
$rc->{'enabled'} ? 2 : 1;
|
|
}
|
|
elsif ($init_mode eq "osx") {
|
|
# Look for a hostconfig entry
|
|
my $ucname = uc($name);
|
|
my %hc;
|
|
&read_env_file($config{'hostconfig'}, \%hc);
|
|
return $hc{$ucname} eq '-YES-' ? 2 :
|
|
$hc{$ucname} eq '-NO-' ? 1 : 0;
|
|
}
|
|
elsif ($init_mode eq "launchd") {
|
|
my @agents = &list_launchd_agents();
|
|
my ($agent) = grep { $_->{'name'} eq &launchd_name($name) } @agents;
|
|
return !$agent ? 0 :
|
|
$agent->{'boot'} ? 2 : 1;
|
|
}
|
|
}
|
|
|
|
=head2 enable_at_boot(action, description, startcode, stopcode, statuscode, &opts)
|
|
|
|
Makes some action start at boot time, creating the script by copying the
|
|
specified file if necessary. The parameters are :
|
|
|
|
=item action - Name of the action to create or enable.
|
|
|
|
=item description - A human-readable description for the action.
|
|
|
|
=item startcode - Shell commands to run at boot time.
|
|
|
|
=item stopcode - Shell commands to run at shutdown time.
|
|
|
|
=item statuscode - Shell code to output the action's status.
|
|
|
|
=item opts - Hash ref of additional options, like : fork -> server will fork into background
|
|
|
|
If this is called for a named action that already exists (even if it isn't
|
|
enabled), only the first parameter needs to be given.
|
|
|
|
=cut
|
|
sub enable_at_boot
|
|
{
|
|
my ($action, $desc, $start, $stop, $status, $opts) = @_;
|
|
my $st = &action_status($action);
|
|
return if ($st == 2); # already exists and is enabled
|
|
my ($daemon, %daemon);
|
|
my $unit = &action_unit($action);
|
|
if ($init_mode eq "upstart" && (!-r "$config{'init_dir'}/$action" ||
|
|
-r "/etc/init/$action.conf")) {
|
|
# Create upstart action if missing, as long as this isn't an old-style
|
|
# init script
|
|
my $cfile = "/etc/init/$action.conf";
|
|
if (-r $cfile) {
|
|
# Config file exists, make sure it is enabled
|
|
if (&has_command("insserv")) {
|
|
&system_logged(
|
|
"insserv ".quotemeta($action)." >/dev/null 2>&1");
|
|
}
|
|
my $lref = &read_file_lines($cfile);
|
|
my $foundstart;
|
|
foreach my $l (@$lref) {
|
|
if ($l =~ /^#+start/) {
|
|
# Start of start block
|
|
$l =~ s/^#+//;
|
|
$foundstart = 1;
|
|
}
|
|
elsif ($l =~ /^#+\s+\S/ && $foundstart) {
|
|
# Continuation line for start
|
|
$l =~ s/^#+//;
|
|
}
|
|
elsif ($l =~ /^\S/ && $foundstart) {
|
|
# Some other directive after start
|
|
last;
|
|
}
|
|
}
|
|
&flush_file_lines($cfile);
|
|
}
|
|
else {
|
|
# Need to create config
|
|
$start || &error("Upstart service $action does not exist");
|
|
&create_upstart_service($action, $desc, $start, undef,
|
|
$opts->{'fork'});
|
|
if (&has_command("insserv")) {
|
|
&system_logged(
|
|
"insserv ".quotemeta($action)." >/dev/null 2>&1");
|
|
}
|
|
}
|
|
return;
|
|
}
|
|
if ($init_mode eq "systemd" && (!-r "$config{'init_dir'}/$action" ||
|
|
&is_systemd_service($unit))) {
|
|
# Create systemd unit if missing, as long as this isn't an old-style
|
|
# init script
|
|
my $st = &action_status($action);
|
|
if ($st == 0) {
|
|
# Need to create config
|
|
$start || &error("Systemd service $action does not exist");
|
|
&create_systemd_service($unit, $desc, $start, $stop, undef,
|
|
$opts->{'fork'}, $opts->{'pidfile'},
|
|
$opts->{'exit'}, $opts->{'opts'});
|
|
}
|
|
&unmask_action($unit);
|
|
&system_logged("systemctl enable ".
|
|
quotemeta($unit)." >/dev/null 2>&1");
|
|
return;
|
|
}
|
|
if ($init_mode eq "init" || $init_mode eq "local" || $init_mode eq "upstart" ||
|
|
$init_mode eq "systemd") {
|
|
# In these modes, we create a script to run
|
|
my $fn;
|
|
if ($init_mode eq "init" || $init_mode eq "upstart" ||
|
|
$init_mode eq "systemd") {
|
|
# Normal init.d system
|
|
$fn = &action_filename($action);
|
|
}
|
|
else {
|
|
# Need to create hack init script
|
|
$fn = "$module_config_directory/$action.sh";
|
|
}
|
|
my @chk = &chkconfig_info($fn);
|
|
my @start = @{$chk[0]} ? @{$chk[0]} : &get_start_runlevels();
|
|
my $start_order = $chk[1] || "9" x $config{'order_digits'};
|
|
my $stop_order = $chk[2] || "9" x $config{'order_digits'};
|
|
my @stop;
|
|
if (@chk) {
|
|
my %starting = map { $_, 1 } @start;
|
|
@stop = grep { !$starting{$_} && /^\d+$/ } &list_runlevels();
|
|
}
|
|
|
|
my $need_links = 0;
|
|
if ($st == 1) {
|
|
# Just need to create links (later)
|
|
$need_links++;
|
|
}
|
|
elsif ($desc) {
|
|
# Need to create the init script
|
|
$start || $stop || &error("Init script $action does not exist");
|
|
&lock_file($fn);
|
|
&open_tempfile(ACTION, ">$fn");
|
|
&print_tempfile(ACTION, "#!/bin/sh\n");
|
|
if ($config{'chkconfig'}) {
|
|
# Redhat-style description: and chkconfig: lines
|
|
&print_tempfile(ACTION, "# description: $desc\n");
|
|
&print_tempfile(ACTION, "# chkconfig: $config{'chkconfig'} ",
|
|
"$start_order $stop_order\n");
|
|
}
|
|
elsif ($config{'init_info'}) {
|
|
# Suse-style init info section
|
|
&print_tempfile(ACTION, "### BEGIN INIT INFO\n",
|
|
"# Provides: $action\n",
|
|
"# Required-Start: \$network \$syslog\n",
|
|
"# Required-Stop: \$network\n",
|
|
"# Default-Start: ",join(" ", @start),"\n",
|
|
"# Default-Stop:\n",
|
|
"# Description: $desc\n",
|
|
"### END INIT INFO\n");
|
|
}
|
|
else {
|
|
&print_tempfile(ACTION, "# $desc\n");
|
|
}
|
|
&print_tempfile(ACTION, "\n");
|
|
&print_tempfile(ACTION, "case \"\$1\" in\n");
|
|
|
|
if ($start) {
|
|
&print_tempfile(ACTION, "'start')\n");
|
|
&print_tempfile(ACTION, &tab_indent($start));
|
|
&print_tempfile(ACTION, "\tRETVAL=\$?\n");
|
|
if ($config{'subsys'}) {
|
|
&print_tempfile(ACTION, "\tif [ \"\$RETVAL\" = \"0\" ]; then\n");
|
|
&print_tempfile(ACTION, "\t\ttouch $config{'subsys'}/$action\n");
|
|
&print_tempfile(ACTION, "\tfi\n");
|
|
}
|
|
&print_tempfile(ACTION, "\t;;\n");
|
|
}
|
|
|
|
if ($stop) {
|
|
&print_tempfile(ACTION, "'stop')\n");
|
|
&print_tempfile(ACTION, &tab_indent($stop));
|
|
&print_tempfile(ACTION, "\tRETVAL=\$?\n");
|
|
if ($config{'subsys'}) {
|
|
&print_tempfile(ACTION, "\tif [ \"\$RETVAL\" = \"0\" ]; then\n");
|
|
&print_tempfile(ACTION, "\t\trm -f $config{'subsys'}/$action\n");
|
|
&print_tempfile(ACTION, "\tfi\n");
|
|
}
|
|
&print_tempfile(ACTION, "\t;;\n");
|
|
}
|
|
|
|
if ($status) {
|
|
&print_tempfile(ACTION, "'status')\n");
|
|
&print_tempfile(ACTION, &tab_indent($status));
|
|
&print_tempfile(ACTION, "\t;;\n");
|
|
}
|
|
|
|
if ($start && $stop) {
|
|
&print_tempfile(ACTION, "'restart')\n");
|
|
&print_tempfile(ACTION, "\t\$0 stop ; \$0 start\n");
|
|
&print_tempfile(ACTION, "\tRETVAL=\$?\n");
|
|
&print_tempfile(ACTION, "\t;;\n");
|
|
}
|
|
|
|
&print_tempfile(ACTION, "*)\n");
|
|
&print_tempfile(ACTION, "\techo \"Usage: \$0 { start | stop }\"\n");
|
|
&print_tempfile(ACTION, "\tRETVAL=1\n");
|
|
&print_tempfile(ACTION, "\t;;\n");
|
|
&print_tempfile(ACTION, "esac\n");
|
|
&print_tempfile(ACTION, "exit \$RETVAL\n");
|
|
&close_tempfile(ACTION);
|
|
chmod(0755, $fn);
|
|
&unlock_file($fn);
|
|
$need_links++;
|
|
}
|
|
|
|
if ($need_links && ($init_mode eq "init" ||
|
|
$init_mode eq "upstart" ||
|
|
$init_mode eq "systemd")) {
|
|
my $data = &read_file_contents($fn);
|
|
my $done = 0;
|
|
if ($use_chkconfig &&
|
|
(@chk && $chk[3] || $data =~ /Default-Start:/i)) {
|
|
# Call the chkconfig command to link up
|
|
&system_logged("chkconfig --add ".quotemeta($action));
|
|
my $ex = &system_logged(
|
|
"chkconfig ".quotemeta($action)." on");
|
|
if (!$ex) {
|
|
$done = 1;
|
|
}
|
|
}
|
|
elsif (&has_command("insserv") && $data =~ /Default-Start:/i) {
|
|
# Call the insserv command to enable
|
|
my $ex = &system_logged("insserv ".quotemeta($action).
|
|
" >/dev/null 2>&1");
|
|
$done = 1 if (!$ex && &action_status($action) == 2);
|
|
}
|
|
if (!$done) {
|
|
# Just link up the init script
|
|
my $s;
|
|
foreach $s (@start) {
|
|
&add_rl_action($action, $s, "S", $start_order);
|
|
}
|
|
my @klevels = &action_levels("K", $action);
|
|
if (!@klevels) {
|
|
# Only add K scripts if none exist
|
|
foreach $s (@stop) {
|
|
&add_rl_action($action, $s, "K", $stop_order);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
elsif ($need_links) {
|
|
# Just add rc.my entry
|
|
my $lref = &read_file_lines($config{'local_script'});
|
|
my $i;
|
|
for($i=0; $i<@$lref && $lref->[$i] !~ /^exit\s/; $i++) { }
|
|
splice(@$lref, $i, 0, "$fn start");
|
|
if ($config{'local_down'}) {
|
|
# Also add to shutdown script
|
|
$lref = &read_file_lines($config{'local_down'});
|
|
for($i=0; $i<@$lref &&
|
|
$lref->[$i] !~ /^exit\s/; $i++) { }
|
|
splice(@$lref, $i, 0, "$fn stop");
|
|
}
|
|
&flush_file_lines();
|
|
}
|
|
}
|
|
elsif ($init_mode eq "win32") {
|
|
# Enable and/or create a win32 service
|
|
if ($st == 1) {
|
|
# Just enable
|
|
&enable_win32_service($action);
|
|
}
|
|
else {
|
|
# Need to create service, which calls wrapper program
|
|
eval "use Win32::Daemon";
|
|
|
|
# modify the string handed over
|
|
# so it does not contain backslashes ...
|
|
$start =~ s/\\/\//g;
|
|
|
|
my $perl_path = &get_perl_path();
|
|
my %svc = ( 'name' => $action,
|
|
'display' => $desc,
|
|
'path' => $perl_path,
|
|
'user' => '',
|
|
'description' => "OCM Webmin Pro Service",
|
|
'pwd' => $module_root_directory,
|
|
'parameters' => "\"$module_root_directory/win32.pl\" $start",
|
|
);
|
|
if (!Win32::Daemon::CreateService(\%svc)) {
|
|
print STDERR "Failed to create Win32 service : ",
|
|
Win32::FormatMessage(Win32::Daemon::GetLastError()),"\n";
|
|
}
|
|
}
|
|
}
|
|
elsif ($init_mode eq "rc") {
|
|
# Enable and/or create an RC script
|
|
&lock_rc_files();
|
|
if ($st == 1) {
|
|
# Just enable
|
|
&enable_rc_script($action);
|
|
}
|
|
else {
|
|
# Need to create a my rc script, and enable
|
|
my @dirs = split(/\s+/, $config{'rc_dir'});
|
|
my $file = $dirs[$#dirs]."/".$action;
|
|
my $name = $action;
|
|
$name =~ s/-/_/g;
|
|
&open_lock_tempfile(SCRIPT, ">$file");
|
|
&print_tempfile(SCRIPT, "#!/bin/sh\n");
|
|
&print_tempfile(SCRIPT, "#\n");
|
|
&print_tempfile(SCRIPT, "# PROVIDE: $action\n");
|
|
&print_tempfile(SCRIPT, "# REQUIRE: LOGIN\n");
|
|
&print_tempfile(SCRIPT, "\n");
|
|
&print_tempfile(SCRIPT, ". /etc/rc.subr\n");
|
|
&print_tempfile(SCRIPT, "\n");
|
|
&print_tempfile(SCRIPT, "name=$name\n");
|
|
&print_tempfile(SCRIPT, "rcvar=`set_rcvar`\n");
|
|
&print_tempfile(SCRIPT, "start_cmd=\"$start\"\n");
|
|
if ($stop) {
|
|
&print_tempfile(SCRIPT, "stop_cmd=\"$stop\"\n")
|
|
}
|
|
if ($status && $status !~ /\n/) {
|
|
&print_tempfile(SCRIPT, "status_cmd=\"$status\"\n")
|
|
}
|
|
&print_tempfile(SCRIPT, "\n");
|
|
&print_tempfile(SCRIPT, "load_rc_config \${name}\n");
|
|
&print_tempfile(SCRIPT, "run_rc_command \"\$1\"\n");
|
|
&close_tempfile(SCRIPT);
|
|
&set_ownership_permissions(undef, undef, 0755, $file);
|
|
&enable_rc_script($action);
|
|
}
|
|
&unlock_rc_files();
|
|
}
|
|
elsif ($init_mode eq "osx") {
|
|
# Add hostconfig file entry
|
|
my $ucname = uc($action);
|
|
my %hc;
|
|
&lock_file($config{'hostconfig'});
|
|
&read_env_file($config{'hostconfig'}, \%hc);
|
|
if (!$hc{$ucname}) {
|
|
# Need to create action
|
|
my $ucfirst = ucfirst($action);
|
|
my $dir = "$config{'darwin_setup'}/$ucfirst";
|
|
my $paramlist = "$dir/$config{'plist'}";
|
|
my $scriptfile = "$dir/$ucfirst";
|
|
|
|
# Create dirs if missing
|
|
if (!-d $config{'darwin_setup'}) {
|
|
&make_dir($config{'darwin_setup'}, 0755);
|
|
}
|
|
if (!-d $dir) {
|
|
&make_dir($dir, 0755);
|
|
}
|
|
|
|
# Make params list file
|
|
&open_lock_tempfile(PLIST, ">$paramlist");
|
|
&print_tempfile(PLIST, "{\n");
|
|
&print_tempfile(PLIST, "\t\tDescription\t\t= \"$desc\";\n");
|
|
&print_tempfile(PLIST, "\t\tProvides\t\t= (\"$ucfirst\");\n");
|
|
&print_tempfile(PLIST, "\t\tRequires\t\t= (\"Resolver\");\n");
|
|
&print_tempfile(PLIST, "\t\tOrderPreference\t\t= \"None\";\n");
|
|
&print_tempfile(PLIST, "\t\tMessages =\n");
|
|
&print_tempfile(PLIST, "\t\t{\n");
|
|
&print_tempfile(PLIST, "\t\t\tstart\t= \"Starting $ucfirst\";\n");
|
|
&print_tempfile(PLIST, "\t\t\tstop\t= \"Stopping $ucfirst\";\n");
|
|
&print_tempfile(PLIST, "\t\t};\n");
|
|
&print_tempfile(PLIST, "}\n");
|
|
&close_tempfile(PLIST);
|
|
|
|
# Create Bootup Script
|
|
&open_lock_tempfile(STARTUP, ">$scriptfile");
|
|
&print_tempfile(STARTUP, "#!/bin/sh\n\n");
|
|
&print_tempfile(STARTUP, ". /etc/rc.common\n\n");
|
|
&print_tempfile(STARTUP, "if [ \"\${$ucname:=-NO-}\" = \"-YES-\" ]; then\n");
|
|
&print_tempfile(STARTUP, "\tConsoleMessage \"Starting $ucfirst\"\n");
|
|
&print_tempfile(STARTUP, "\t$start\n");
|
|
&print_tempfile(STARTUP, "fi\n");
|
|
&close_tempfile(STARTUP);
|
|
&set_ownership_permissions(undef, undef, 0750, $scriptfile);
|
|
}
|
|
|
|
# Update hostconfig file
|
|
$hc{$ucname} = '-YES-';
|
|
&write_env_file($config{'hostconfig'}, \%hc);
|
|
&unlock_file($config{'hostconfig'});
|
|
}
|
|
elsif ($init_mode eq "launchd") {
|
|
# Create and if necessary enable a launchd agent
|
|
my $name = &launchd_name($action);
|
|
my @agents = &list_launchd_agents();
|
|
my ($agent) = grep { $_->{'name'} eq $name } @agents;
|
|
if (!$agent) {
|
|
# Need to create script
|
|
&create_launchd_agent($name, $desc, 1);
|
|
}
|
|
else {
|
|
# Just enable at boot
|
|
my $out = &read_file_contents($agent->{'file'});
|
|
if ($out =~ /<key>RunAtLoad<\/key>/i) {
|
|
# Just fix setting
|
|
$out =~ s/<key>RunAtLoad<\/key>\s*<(true|false)\/>/<key>RunAtLoad<\/key>\n<true\/>/;
|
|
}
|
|
else {
|
|
# Defaults to false, so need to add before </plist>
|
|
$out =~ s/<\/plist>/<key>RunAtLoad<\/key>\n<true\/>\n<\/plist>/;
|
|
}
|
|
&open_lock_tempfile(PLIST, ">$agent->{'file'}");
|
|
&print_tempfile(PLIST, $out);
|
|
&close_tempfile(PLIST);
|
|
}
|
|
}
|
|
}
|
|
|
|
=head2 enable_at_boot_as_user(action, description, startcode, stopcode, statuscode, &opts, user)
|
|
|
|
Like enable_at_boot, but runs the startup command as the given user.
|
|
|
|
=cut
|
|
sub enable_at_boot_as_user
|
|
{
|
|
my ($action, $desc, $start, $stop, $status, $opts, $user) = @_;
|
|
if ($user && $user ne "root") {
|
|
if ($init_mode eq "systemd") {
|
|
# Systemd natively supports running the command as a user
|
|
$opts ||= { };
|
|
$opts->{'opts'} ||= { };
|
|
$opts->{'opts'}->{'user'} = $user;
|
|
}
|
|
else {
|
|
# Other boot systems have to use 'su'
|
|
$start = &command_as_user($user, 0, $start);
|
|
$stop = &command_as_user($user, 0, $stop) if ($stop);
|
|
$status = &command_as_user($user, 0, $status) if ($status);
|
|
}
|
|
}
|
|
return &enable_at_boot($action, $desc, $start, $stop, $status, $opts);
|
|
}
|
|
|
|
=head2 disable_at_boot(action)
|
|
|
|
Disabled some action from starting at boot, identified by the action
|
|
parameter. The config files that define what commands the action runs are not
|
|
touched, so it can be re-enabled with the enable_at_boot function.
|
|
|
|
=cut
|
|
sub disable_at_boot
|
|
{
|
|
my ($name) = @_;
|
|
my $st = &action_status($_[0]);
|
|
return if ($st == 0); # does not exist
|
|
my $unit = &action_unit($_[0]);
|
|
if ($init_mode eq "upstart") {
|
|
# Just use insserv to disable, and comment out start line in .conf file
|
|
if (&has_command("insserv")) {
|
|
&system_logged(
|
|
"insserv -r ".quotemeta($_[0])." >/dev/null 2>&1");
|
|
}
|
|
my $cfile = "/etc/init/$_[0].conf";
|
|
if (-r $cfile) {
|
|
my $lref = &read_file_lines($cfile);
|
|
my $foundstart;
|
|
foreach my $l (@$lref) {
|
|
if ($l =~ /^start\s/) {
|
|
# Start of start block
|
|
$l = "#".$l;
|
|
$foundstart = 1;
|
|
}
|
|
elsif ($l =~ /^\s+\S/ && $foundstart) {
|
|
# Continuation line for start
|
|
$l = "#".$l;
|
|
}
|
|
elsif ($l =~ /^\S/ && $foundstart) {
|
|
# Some other directive after start
|
|
last;
|
|
}
|
|
}
|
|
&flush_file_lines($cfile);
|
|
}
|
|
}
|
|
elsif ($init_mode eq "systemd") {
|
|
# Use systemctl to disable at boot
|
|
&system_logged("systemctl disable ".quotemeta($unit).
|
|
" >/dev/null 2>&1");
|
|
}
|
|
if ($init_mode eq "init" || $init_mode eq "upstart" ||
|
|
$init_mode eq "systemd") {
|
|
# Unlink or disable init script
|
|
my $file = &action_filename($_[0]);
|
|
my @chk = &chkconfig_info($file);
|
|
my $data = &read_file_contents($file);
|
|
|
|
if ($use_chkconfig && @chk) {
|
|
# Call chkconfig to remove the links
|
|
&system_logged("chkconfig ".quotemeta($_[0])." off");
|
|
}
|
|
else {
|
|
# Just unlink the S links
|
|
foreach my $a (&action_levels('S', $_[0])) {
|
|
$a =~ /^(\S+)\s+(\S+)\s+(\S+)$/;
|
|
&delete_rl_action($_[0], $1, 'S');
|
|
}
|
|
|
|
if (@chk) {
|
|
# Take out the K links as well, since we know how to put
|
|
# them back from the chkconfig info
|
|
foreach my $a (&action_levels('K', $_[0])) {
|
|
$a =~ /^(\S+)\s+(\S+)\s+(\S+)$/;
|
|
&delete_rl_action($_[0], $1, 'K');
|
|
}
|
|
}
|
|
}
|
|
}
|
|
elsif ($init_mode eq "local") {
|
|
# Take out of rc.my file
|
|
my $lref = &read_file_lines($config{'local_script'});
|
|
my $cmd = "$module_config_directory/$_[0].sh start";
|
|
my $i;
|
|
for($i=0; $i<@$lref; $i++) {
|
|
if ($lref->[$i] eq $cmd) {
|
|
splice(@$lref, $i, 1);
|
|
last;
|
|
}
|
|
}
|
|
if ($config{'local_down'}) {
|
|
# Take out of shutdown script
|
|
$lref = &read_file_lines($config{'local_down'});
|
|
my $cmd = "$module_config_directory/$_[0].sh stop";
|
|
for($i=0; $i<@$lref; $i++) {
|
|
if ($lref->[$i] eq $cmd) {
|
|
splice(@$lref, $i, 1);
|
|
last;
|
|
}
|
|
}
|
|
}
|
|
&flush_file_lines();
|
|
}
|
|
elsif ($init_mode eq "win32") {
|
|
# Disable the service
|
|
&disable_win32_service($_[0]);
|
|
}
|
|
elsif ($init_mode eq "rc") {
|
|
# Disable an RC script
|
|
&lock_rc_files();
|
|
&disable_rc_script($_[0]);
|
|
&unlock_rc_files();
|
|
}
|
|
elsif ($init_mode eq "osx") {
|
|
# Disable in hostconfig
|
|
my $ucname = uc($_[0]);
|
|
my %hc;
|
|
&lock_file($config{'hostconfig'});
|
|
&read_env_file($config{'hostconfig'}, \%hc);
|
|
if ($hc{$ucname} eq '-YES-' || $hc{$ucname} eq '-AUTOMATIC-') {
|
|
$hc{$ucname} = '-NO-';
|
|
&write_env_file($config{'hostconfig'}, \%hc);
|
|
}
|
|
&unlock_file($config{'hostconfig'});
|
|
}
|
|
elsif ($init_mode eq "launchd") {
|
|
# Adjust plist file to not run at boot
|
|
my @agents = &list_launchd_agents();
|
|
my ($a) = grep { $_->{'name'} eq &launchd_name($name) } @agents;
|
|
if ($a && $a->{'file'}) {
|
|
my $out = &read_file_contents($a->{'file'});
|
|
$out =~ s/<key>RunAtLoad<\/key>\s*<(true|false)\/>/<key>RunAtLoad<\/key>\n<false\/>/;
|
|
&open_lock_tempfile(PLIST, ">$a->{'file'}");
|
|
&print_tempfile(PLIST, $out);
|
|
&close_tempfile(PLIST);
|
|
}
|
|
}
|
|
}
|
|
|
|
=head2 activate_action(name)
|
|
|
|
Unmasks some action, enables it at boot time, and starts it if not running.
|
|
Returns 1 if the action exists, 0 if not.
|
|
|
|
=cut
|
|
sub activate_action
|
|
{
|
|
my ($name) = @_;
|
|
my $st = &action_status($name);
|
|
return 0 if (!$st);
|
|
&unmask_action($name);
|
|
&enable_at_boot($name);
|
|
my $running = &status_action($name);
|
|
if ($running != 1) { # unknown or stopped
|
|
&start_action($name);
|
|
}
|
|
return 1;
|
|
}
|
|
|
|
=head2 deactivate_action(name, [mask])
|
|
|
|
Stops some action if currently running, disables it at boot time, and masks it
|
|
on systemd systems. The optional mask flag can be set to 0 to skip masking.
|
|
Returns 1 if the action exists, 0 if not.
|
|
|
|
=cut
|
|
sub deactivate_action
|
|
{
|
|
my ($name, $mask) = @_;
|
|
my $st = &action_status($name);
|
|
return 0 if (!$st);
|
|
my $running = &status_action($name);
|
|
if ($running != 0) { # unknown or running
|
|
&stop_action($name);
|
|
}
|
|
&disable_at_boot($name);
|
|
&mask_action($name) if (!defined($mask) || $mask);
|
|
return 1;
|
|
}
|
|
|
|
=head2 delete_at_boot(name)
|
|
|
|
Delete the init script, RC script or whatever with some name
|
|
|
|
=cut
|
|
sub delete_at_boot
|
|
{
|
|
my ($name) = @_;
|
|
my $mode = &get_action_mode($name);
|
|
if ($mode eq "systemd") {
|
|
# Delete systemd unit
|
|
&delete_systemd_service($name);
|
|
}
|
|
elsif ($mode eq "upstart") {
|
|
# Delete upstart service
|
|
&delete_upstart_service($name);
|
|
}
|
|
elsif ($mode eq "launchd") {
|
|
# Delete launchd service
|
|
&delete_launchd_agent(&launchd_name($name));
|
|
}
|
|
elsif ($mode eq "init") {
|
|
# Delete init script links and init.d file
|
|
foreach my $a (&action_levels('S', $name)) {
|
|
$a =~ /^(\S+)\s+(\S+)\s+(\S+)$/ &&
|
|
&delete_rl_action($name, $1, 'S');
|
|
}
|
|
foreach my $a (&action_levels('K', $name)) {
|
|
$a =~ /^(\S+)\s+(\S+)\s+(\S+)$/ &&
|
|
&delete_rl_action($name, $1, 'K');
|
|
}
|
|
my $fn = &action_filename($name);
|
|
&unlink_logged($fn);
|
|
}
|
|
elsif ($mode eq "win32") {
|
|
# Delete windows service
|
|
&delete_win32_service($name);
|
|
}
|
|
elsif ($mode eq "rc") {
|
|
# Delete FreeBSD RC script
|
|
&delete_rc_script($name);
|
|
}
|
|
elsif ($mode eq "osx") {
|
|
# Delete OSX hostconfig entry
|
|
open(LOCAL, "<".$config{'hostconfig'});
|
|
my @my = <LOCAL>;
|
|
close(LOCAL);
|
|
my $start = $name."=-";
|
|
&open_tempfile(LOCAL, ">$config{'hostconfig'}");
|
|
&print_tempfile(LOCAL, grep { !/^$start/i } @local);
|
|
&close_tempfile(LOCAL);
|
|
my $paramlist = "$config{'darwin_setup'}/$ucproduct/$config{'plist'}";
|
|
my $scriptfile = "$config{'darwin_setup'}/$ucproduct/$ucproduct";
|
|
&unlink_logged($paramlist);
|
|
&unlink_logged($scriptfile);
|
|
}
|
|
elsif ($mode eq "local") {
|
|
# Delete from my rc file
|
|
&disable_at_boot($name);
|
|
}
|
|
}
|
|
|
|
=head2 start_action(name)
|
|
|
|
Start the action with the given name, using whatever method is appropriate
|
|
for this operating system. Returns a status code (0 or 1 for failure or
|
|
success) and all output from the action script.
|
|
|
|
=cut
|
|
sub start_action
|
|
{
|
|
my ($name) = @_;
|
|
my $action_mode = &get_action_mode($name);
|
|
if ($action_mode eq "init" || $action_mode eq "local") {
|
|
# Run the init script or Webmin-created wrapper
|
|
my $fn = $action_mode eq "init" ? &action_filename($name) :
|
|
"$module_config_directory/$name.sh";
|
|
if (!-x $fn) {
|
|
return (0, "$fn does not exist");
|
|
}
|
|
&clean_environment();
|
|
my $out = &backquote_logged("$fn start 2>&1 </dev/null");
|
|
&reset_environment();
|
|
my $ex = $?;
|
|
return (!$ex, $out);
|
|
}
|
|
elsif ($action_mode eq "rc") {
|
|
# Run FreeBSD RC script
|
|
return &start_rc_script($name);
|
|
}
|
|
elsif ($action_mode eq "win32") {
|
|
# Start Windows service
|
|
my $err = &start_win32_service($name);
|
|
return (!$err, $err);
|
|
}
|
|
elsif ($action_mode eq "upstart") {
|
|
# Run upstart action
|
|
return &start_upstart_service($name);
|
|
}
|
|
elsif ($action_mode eq "systemd") {
|
|
# Start systemd service
|
|
return &start_systemd_service($name);
|
|
}
|
|
elsif ($action_mode eq "launchd") {
|
|
# Start launchd service
|
|
return &start_launchd_agent(&launchd_name($name));
|
|
}
|
|
else {
|
|
return (0, "Bootup mode $action_mode not supported");
|
|
}
|
|
}
|
|
|
|
=head2 stop_action(name)
|
|
|
|
Stop the action with the given name, using whatever method is appropriate
|
|
for this operating system. Returns a status code (0 or 1 for failure or
|
|
success) and all output from the action script.
|
|
|
|
=cut
|
|
sub stop_action
|
|
{
|
|
my ($name) = @_;
|
|
my $action_mode = &get_action_mode($name);
|
|
if ($action_mode eq "init" || $action_mode eq "local") {
|
|
# Run the init script or Webmin-created wrapper
|
|
my $fn = $action_mode eq "init" ? &action_filename($name) :
|
|
"$module_config_directory/$name.sh";
|
|
if (!-x $fn) {
|
|
return (0, "$fn does not exist");
|
|
}
|
|
my $out = &backquote_logged("$fn stop 2>&1 </dev/null");
|
|
my $ex = $?;
|
|
return (!$ex, $out);
|
|
}
|
|
elsif ($action_mode eq "rc") {
|
|
# Run FreeBSD RC script
|
|
return &stop_rc_script($name);
|
|
}
|
|
elsif ($action_mode eq "win32") {
|
|
# Start Windows service
|
|
my $err = &stop_win32_service($name);
|
|
return (!$err, $err);
|
|
}
|
|
elsif ($action_mode eq "upstart") {
|
|
# Stop upstart action
|
|
return &stop_upstart_service($name);
|
|
}
|
|
elsif ($action_mode eq "systemd") {
|
|
# Stop systemd service
|
|
return &stop_systemd_service($name);
|
|
}
|
|
elsif ($action_mode eq "launchd") {
|
|
# Stop launchd service
|
|
return &stop_launchd_agent(&launchd_name($name));
|
|
}
|
|
else {
|
|
return (0, "Bootup mode $action_mode not supported");
|
|
}
|
|
}
|
|
|
|
=head2 restart_action(action)
|
|
|
|
Calls a stop then a start for some named action.
|
|
|
|
=cut
|
|
sub restart_action
|
|
{
|
|
my ($name) = @_;
|
|
my $action_mode = &get_action_mode($name);
|
|
if ($action_mode eq "upstart") {
|
|
return &restart_upstart_service($name);
|
|
}
|
|
elsif ($action_mode eq "systemd") {
|
|
return &restart_systemd_service($name);
|
|
}
|
|
else {
|
|
&stop_action($name);
|
|
return &start_action($name);
|
|
}
|
|
}
|
|
|
|
=head2 reload_action(action)
|
|
|
|
Does a config reload for some action.
|
|
|
|
=cut
|
|
sub reload_action
|
|
{
|
|
my ($name) = @_;
|
|
my $action_mode = &get_action_mode($name);
|
|
if ($action_mode eq "upstart") {
|
|
return &reload_upstart_service($name);
|
|
}
|
|
elsif ($action_mode eq "systemd") {
|
|
return &reload_systemd_service($name);
|
|
}
|
|
elsif ($action_mode eq "init") {
|
|
my $file = &action_filename($name);
|
|
my $hasarg = &get_action_args($file);
|
|
if ($hasarg->{'reload'}) {
|
|
my $cmd = $file." reload";
|
|
my $out = &backquote_logged("$cmd 2>&1 </dev/null");
|
|
return $? ? (0, $out) : (1, undef);
|
|
}
|
|
}
|
|
return (0, "Not implemented");
|
|
}
|
|
|
|
=head2 status_action(name)
|
|
|
|
Returns 1 if some action is running right now, 0 if not, or -1 if unknown
|
|
|
|
=cut
|
|
sub status_action
|
|
{
|
|
my ($name) = @_;
|
|
my $action_mode = &get_action_mode($name);
|
|
if ($action_mode eq "init") {
|
|
# Run init script to get status
|
|
return &action_running(&action_filename($name));
|
|
}
|
|
elsif ($action_mode eq "win32") {
|
|
# Check with Windows if it is running
|
|
my ($w) = &list_win32_services($name);
|
|
return !$w ? -1 : $w->{'status'} == 4 ? 1 : 0;
|
|
}
|
|
elsif ($action_mode eq "upstart") {
|
|
# Check with upstart if it is running
|
|
my @upstarts = &list_upstart_services();
|
|
my ($u) = grep { $_->{'name'} eq $name } @upstarts;
|
|
return !$u ? -1 : $u>{'status'} eq 'running' ? 1 :
|
|
$u->{'status'} eq 'waiting' ? 0 : -1;
|
|
}
|
|
elsif ($action_mode eq "systemd") {
|
|
# Check with systemd if it is running
|
|
my $out = &backquote_command(
|
|
"systemctl is-failed ".quotemeta($name)." 2>/dev/null");
|
|
$out =~ s/\r?\n//g;
|
|
$out = lc($out);
|
|
return $out eq "active" ? 1 :
|
|
$out eq "inactive" || $out eq "failed" ||
|
|
$out eq "active (exited)" ? 0 : -1;
|
|
}
|
|
elsif ($action_mode eq "launchd") {
|
|
my @agents = &list_launchd_agents();
|
|
my ($a) = grep { $_->{'name'} eq &launchd_name($name) } @agents;
|
|
return !$u ? -1 : $u->{'status'} ? 1 : 0;
|
|
}
|
|
else {
|
|
return -1;
|
|
}
|
|
}
|
|
|
|
=head2 mask_action(name)
|
|
|
|
Mask systemd target
|
|
|
|
=cut
|
|
sub mask_action
|
|
{
|
|
my ($name) = @_;
|
|
$name = &action_unit($name);
|
|
return -1 if (!&is_systemd_service($name));
|
|
if ($init_mode eq "systemd") {
|
|
return &system_logged("systemctl mask ".
|
|
quotemeta($name)." >/dev/null 2>&1");
|
|
}
|
|
return -1;
|
|
}
|
|
|
|
=head2 unmask_action(name)
|
|
|
|
Unmask systemd target
|
|
|
|
=cut
|
|
sub unmask_action
|
|
{
|
|
my ($name) = @_;
|
|
$name = &action_unit($name);
|
|
if ($init_mode eq "systemd") {
|
|
return &system_logged("systemctl unmask ".
|
|
quotemeta($name)." >/dev/null 2>&1");
|
|
}
|
|
return -1;
|
|
}
|
|
|
|
=head2 list_action_names()
|
|
|
|
Returns a list of just action names
|
|
|
|
=cut
|
|
sub list_action_names
|
|
{
|
|
if ($init_mode eq "upstart") {
|
|
return map { $_->{'name'} } &list_upstart_services();
|
|
}
|
|
elsif ($init_mode eq "systemd") {
|
|
return map { $_->{'name'} } &list_systemd_services();
|
|
}
|
|
elsif ($init_mode eq "init") {
|
|
return map { my @w = split(/\s+/, $_); $w[0] } &list_actions();
|
|
}
|
|
elsif ($init_mode eq "win32") {
|
|
return map { $_->{'name'} } &list_win32_services();
|
|
}
|
|
elsif ($init_mode eq "rc") {
|
|
return map { $_->{'name'} } &list_rc_scripts();
|
|
}
|
|
elsif ($init_mode eq "launchd") {
|
|
return map { $_->{'name'} } &list_launchd_agents();
|
|
}
|
|
return ( );
|
|
}
|
|
|
|
=head2 get_action_mode(name)
|
|
|
|
Returns the init mode used by some action. May be different from the global
|
|
default on systems with mixed modes
|
|
|
|
=cut
|
|
sub get_action_mode
|
|
{
|
|
my ($name) = @_;
|
|
if ($init_mode eq "systemd") {
|
|
# If classic init script exists but no systemd unit, assume init
|
|
if (-r "$config{'init_dir'}/$name" && !&is_systemd_service($name)) {
|
|
return "init";
|
|
}
|
|
}
|
|
elsif ($init_mode eq "upstart") {
|
|
# If classic init script exists but not upstart config, assume init
|
|
if (-r "$config{'init_dir'}/$name" && !-r "/etc/init/$name.conf") {
|
|
return "init";
|
|
}
|
|
}
|
|
return $init_mode;
|
|
}
|
|
|
|
=head2 tab_indent(lines)
|
|
|
|
Given a string with multiple \n separated lines, returns the same string
|
|
with lines prefixed by tabs.
|
|
|
|
=cut
|
|
sub tab_indent
|
|
{
|
|
my ($rv, $l);
|
|
foreach $l (split(/\n/, $_[0])) {
|
|
$rv .= "\t$l\n";
|
|
}
|
|
return $rv;
|
|
}
|
|
|
|
=head2 get_start_runlevels
|
|
|
|
Returns a list of runlevels that actions should be started in, either based
|
|
on the module configuration or /etc/inittab.
|
|
|
|
=cut
|
|
sub get_start_runlevels
|
|
{
|
|
if ($config{'boot_levels'}) {
|
|
return split(/[ ,]+/, $config{'boot_levels'});
|
|
}
|
|
else {
|
|
my @boot = &get_inittab_runlevel();
|
|
return ( $boot[0] );
|
|
}
|
|
}
|
|
|
|
=head2 runlevel_dir(runlevel)
|
|
|
|
Given a runlevel like 3, returns the directory containing symlinks for it,
|
|
like /etc/rc2.d.
|
|
|
|
=cut
|
|
sub runlevel_dir
|
|
{
|
|
if ($_[0] eq "boot") {
|
|
return "$config{init_base}/boot.d";
|
|
}
|
|
else {
|
|
return "$config{init_base}/rc$_[0].d";
|
|
}
|
|
}
|
|
|
|
=head2 list_win32_services([name])
|
|
|
|
Returns a list of known Win32 services, each of which is a hash ref. If the
|
|
name parameter is given, only details of that service are returned. Useful
|
|
keys for each hash are :
|
|
|
|
=item name - A unique name for the service.
|
|
|
|
=item desc - A human-readable description.
|
|
|
|
=item boot - Set to 2 if started at boot, 3 if not, 4 if disabled.
|
|
|
|
=item state -Set to 4 if running now, 1 if stopped.
|
|
|
|
=cut
|
|
sub list_win32_services
|
|
{
|
|
my ($name) = @_;
|
|
my @rv;
|
|
my $svc;
|
|
|
|
# Get the current statuses
|
|
if ($name) {
|
|
&open_execute_command(SC, "sc query $name", 1, 1);
|
|
}
|
|
else {
|
|
&open_execute_command(SC, "sc query type= service state= all", 1, 1);
|
|
}
|
|
while(<SC>) {
|
|
s/\r|\n//g;
|
|
if (/^SERVICE_NAME:\s+(\S.*\S)/) {
|
|
$svc = { 'name' => $1 };
|
|
push(@rv, $svc);
|
|
}
|
|
elsif (/^DISPLAY_NAME:\s+(\S.*)/ && $svc) {
|
|
$svc->{'desc'} = $1;
|
|
}
|
|
elsif (/^\s+TYPE\s+:\s+(\d+)\s+(\S+)/ && $svc) {
|
|
$svc->{'type'} = $1;
|
|
$svc->{'type_desc'} = $2;
|
|
}
|
|
elsif (/^\s+STATE\s+:\s+(\d+)\s+(\S+)/ && $svc) {
|
|
$svc->{'state'} = $1;
|
|
$svc->{'state_desc'} = $2;
|
|
}
|
|
}
|
|
close(SC);
|
|
|
|
# For each service, see if it starts at boot or not
|
|
foreach $svc (@rv) {
|
|
&open_execute_command(SC, "sc qc \"$svc->{'name'}\"", 1, 1);
|
|
while(<SC>) {
|
|
s/\r|\n//g;
|
|
if (/^\s+START_TYPE\s+:\s+(\d+)\s+(\S+)/) {
|
|
$svc->{'boot'} = $1;
|
|
$svc->{'boot_desc'} = $2;
|
|
}
|
|
}
|
|
close(SC);
|
|
}
|
|
|
|
return @rv;
|
|
}
|
|
|
|
=head2 start_win32_service(name)
|
|
|
|
Attempts to start a service, returning undef on success, or some error message.
|
|
|
|
=cut
|
|
sub start_win32_service
|
|
{
|
|
my ($name) = @_;
|
|
my $out = &backquote_command("sc start \"$name\" 2>&1");
|
|
return $? ? $out : undef;
|
|
}
|
|
|
|
=head2 stop_win32_service(name)
|
|
|
|
Attempts to stop a service, returning undef on success, or some error message.
|
|
|
|
=cut
|
|
sub stop_win32_service
|
|
{
|
|
my ($name) = @_;
|
|
my $out = &backquote_command("sc stop \"$name\" 2>&1");
|
|
return $? ? $out : undef;
|
|
}
|
|
|
|
=head2 enable_win32_service(name)
|
|
|
|
Marks some service as starting at boot time. Returns undef on success or an
|
|
error message on failure.
|
|
|
|
=cut
|
|
sub enable_win32_service
|
|
{
|
|
my ($name) = @_;
|
|
my $out = &backquote_command("sc config \"$name\" start= auto 2>&1");
|
|
return $? ? $out : undef;
|
|
}
|
|
|
|
=head2 disable_win32_service(name)
|
|
|
|
Marks some service as disabled at boot time. Returns undef on success or an
|
|
error message on failure.
|
|
|
|
=cut
|
|
sub disable_win32_service
|
|
{
|
|
my ($name) = @_;
|
|
my $out = &backquote_command("sc config \"$name\" start= demand 2>&1");
|
|
return $? ? $out : undef;
|
|
}
|
|
|
|
=head2 create_win32_service(name, command, desc)
|
|
|
|
Creates a new win32 service, enabled at boot time. The required parameters are:
|
|
name - A unique name for the service
|
|
command - The DOS command to run at boot time
|
|
desc - A human-readable description.
|
|
|
|
=cut
|
|
sub create_win32_service
|
|
{
|
|
my ($name, $cmd, $desc) = @_;
|
|
my $out = &backquote_command("sc create \"$name\" DisplayName= \"$desc\" type= share start= auto binPath= \"$cmd\" 2>&1");
|
|
return $? ? $out : undef;
|
|
}
|
|
|
|
=head2 delete_win32_service(name)
|
|
|
|
Delete some existing service, identified by some name. Returns undef on
|
|
success or an error message on failure.
|
|
|
|
=cut
|
|
sub delete_win32_service
|
|
{
|
|
my ($name) = @_;
|
|
my $out = &backquote_command("sc delete \"$name\" 2>&1");
|
|
return $? ? $out : undef;
|
|
}
|
|
|
|
=head2 list_rc_scripts
|
|
|
|
Returns a list of known BSD RC scripts, and their enabled statuses. Each
|
|
element of the return list is a hash ref, with the following keys :
|
|
|
|
=item name - A unique name for the script.
|
|
|
|
=item desc - A human-readable description.
|
|
|
|
=item enabled - Set to 1 if enabled, 0 if not, 2 if unknown.
|
|
|
|
=item file - Full path to the action script file.
|
|
|
|
=item standard - Set to 0 for user-defined actions, 1 for those supplied with FreeBSD.
|
|
|
|
=cut
|
|
sub list_rc_scripts
|
|
{
|
|
# Build a list of those that are enabled in the rc.conf files
|
|
my @rc = &get_rc_conf();
|
|
my (%enabled, %cmt);
|
|
foreach my $r (@rc) {
|
|
if ($r->{'name'} =~ /^(\S+)_enable$/) {
|
|
my $name = $1;
|
|
if (lc($r->{'value'}) eq 'yes') {
|
|
$enabled{$name} = 1;
|
|
}
|
|
$r->{'cmt'} =~ s/\s*\(\s*or\s+NO\)//i;
|
|
$r->{'cmt'} =~ s/\s*\(YES.*NO\)//i;
|
|
$cmt{$name} ||= $r->{'cmt'};
|
|
}
|
|
}
|
|
|
|
# Scan the script dirs
|
|
my @rv;
|
|
foreach my $dir (split(/\s+/, $config{'rc_dir'})) {
|
|
opendir(DIR, $dir);
|
|
foreach my $f (readdir(DIR)) {
|
|
next if ($f =~ /^\./ || $f =~ /\.(bak|tmp)/i);
|
|
next if (uc($f) eq $f); # Dummy actions are upper-case
|
|
my $name = $f;
|
|
$name =~ s/\.sh$//;
|
|
my $data = &read_file_contents("$dir/$f");
|
|
my $ename = $name;
|
|
$ename =~ s/-/_/g;
|
|
push(@rv, { 'name' => $name,
|
|
'file' => "$dir/$f",
|
|
'enabled' => $data !~ /rc\.subr/ ? 2 :
|
|
$enabled{$ename},
|
|
'startstop' => $data =~ /rc\.subr/ ||
|
|
$data =~ /start\)/,
|
|
'desc' => $cmt{$name},
|
|
'standard' => ($dir !~ /local/)
|
|
});
|
|
}
|
|
closedir(DIR);
|
|
}
|
|
return sort { $a->{'name'} cmp $b->{'name'} } @rv;
|
|
}
|
|
|
|
=head2 save_rc_conf(name, value)
|
|
|
|
Internal function to modify the value of a single entry in the FreeBSD
|
|
rc.conf file.
|
|
|
|
=cut
|
|
sub save_rc_conf
|
|
{
|
|
my $found;
|
|
my @rcs = split(/\s+/, $config{'rc_conf'});
|
|
my $rcfile = $rcs[$#rcs];
|
|
&open_readfile(CONF, $rcfile);
|
|
my @conf = <CONF>;
|
|
close(CONF);
|
|
&open_tempfile(CONF, ">$rcfile");
|
|
foreach (@conf) {
|
|
if (/^\s*([^=]+)\s*=\s*(.*)/ && $1 eq $_[0]) {
|
|
&print_tempfile(CONF, "$_[0]=\"$_[1]\"\n") if (@_ > 1);
|
|
$found++;
|
|
}
|
|
else {
|
|
&print_tempfile(CONF, $_);
|
|
}
|
|
}
|
|
if (!$found && @_ > 1) {
|
|
&print_tempfile(CONF, "$_[0]=\"$_[1]\"\n");
|
|
}
|
|
&close_tempfile(CONF);
|
|
}
|
|
|
|
=head2 get_rc_conf
|
|
|
|
Reads the default and system-specific FreeBSD rc.conf files, and parses
|
|
them into a list of hash refs. Each element in the list has the following keys:
|
|
|
|
=item name - Name of this configuration parameter. May appear more than once, with the later one taking precedence.
|
|
|
|
=item value - Current value.
|
|
|
|
=item cmt - A human-readable comment about the parameter.
|
|
|
|
=cut
|
|
sub get_rc_conf
|
|
{
|
|
my ($file, @rv);
|
|
foreach $file (map { glob($_) } split(/\s+/, $config{'rc_conf'})) {
|
|
my $lnum = 0;
|
|
&open_readfile(FILE, $file);
|
|
while(<FILE>) {
|
|
my $cmt;
|
|
s/\r|\n//g;
|
|
if (s/#(.*)$//) {
|
|
$cmt = $1;
|
|
}
|
|
if (/^\s*([^=\s]+)\s*=\s*"(.*)"/ ||
|
|
/^\s*([^=\s]+)\s*=\s*'(.*)'/ ||
|
|
/^\s*([^=\s]+)\s*=\s*(\S+)/) {
|
|
push(@rv, { 'name' => $1,
|
|
'value' => $2,
|
|
'line' => $lnum,
|
|
'file' => $file,
|
|
'cmt' => $cmt });
|
|
}
|
|
$lnum++;
|
|
}
|
|
close(FILE);
|
|
}
|
|
return @rv;
|
|
}
|
|
|
|
=head2 enable_rc_script(name)
|
|
|
|
Mark some RC script as enabled at boot.
|
|
|
|
=cut
|
|
sub enable_rc_script
|
|
{
|
|
my ($name) = @_;
|
|
$name =~ s/-/_/g;
|
|
&save_rc_conf($name."_enable", "YES");
|
|
}
|
|
|
|
=head2 disable_rc_script(name)
|
|
|
|
Mark some RC script as disabled at boot.
|
|
|
|
=cut
|
|
sub disable_rc_script
|
|
{
|
|
my ($name) = @_;
|
|
$name =~ s/-/_/g;
|
|
my $enabled;
|
|
foreach my $r (&get_rc_conf()) {
|
|
if ($r->{'name'} eq $name."_enable" &&
|
|
lc($r->{'value'}) eq 'yes') {
|
|
$enabled = 1;
|
|
}
|
|
}
|
|
&save_rc_conf($name."_enable", "NO") if ($enabled);
|
|
}
|
|
|
|
=head2 start_rc_script(name)
|
|
|
|
Attempt to start some RC script, and returns 1 or 0 (for success or failure)
|
|
and the output.
|
|
|
|
=cut
|
|
sub start_rc_script
|
|
{
|
|
my ($name) = @_;
|
|
my @rcs = &list_rc_scripts();
|
|
my ($rc) = grep { $_->{'name'} eq $name } @rcs;
|
|
$rc || return "No script found for $name";
|
|
my $out = &backquote_logged("$rc->{'file'} forcestart 2>&1 </dev/null");
|
|
return (!$?, $out);
|
|
}
|
|
|
|
=head2 stop_rc_script(name)
|
|
|
|
Attempts to stop some RC script, and returns 1 or 0 (for success or failure)
|
|
and the output.
|
|
|
|
=cut
|
|
sub stop_rc_script
|
|
{
|
|
my ($name) = @_;
|
|
my @rcs = &list_rc_scripts();
|
|
my ($rc) = grep { $_->{'name'} eq $name } @rcs;
|
|
$rc || return "No script found for $name";
|
|
my $out = &backquote_logged("$rc->{'file'} forcestop 2>&1 </dev/null");
|
|
return (!$?, $out);
|
|
}
|
|
|
|
=head2 delete_rc_script(name)
|
|
|
|
Delete the FreeBSD RC script with some name
|
|
|
|
=cut
|
|
sub delete_rc_script
|
|
{
|
|
my ($name) = @_;
|
|
my @rcs = &list_rc_scripts();
|
|
my ($rc) = grep { $_->{'name'} eq $name } @rcs;
|
|
if ($rc) {
|
|
&lock_rc_files();
|
|
&disable_rc_script($in{'name'});
|
|
&unlock_rc_files();
|
|
&unlink_logged($rc->{'file'});
|
|
}
|
|
}
|
|
|
|
=head2 lock_rc_files
|
|
|
|
Internal function to lock all FreeBSD rc.conf files.
|
|
|
|
=cut
|
|
sub lock_rc_files
|
|
{
|
|
foreach my $f (split(/\s+/, $config{'rc_conf'})) {
|
|
&lock_file($f);
|
|
}
|
|
}
|
|
|
|
=head2 unlock_rc_files
|
|
|
|
Internal function to un-lock all FreeBSD rc.conf files.
|
|
|
|
=cut
|
|
sub unlock_rc_files
|
|
{
|
|
foreach my $f (split(/\s+/, $config{'rc_conf'})) {
|
|
&unlock_file($f);
|
|
}
|
|
}
|
|
|
|
=head2 list_upstart_services
|
|
|
|
Returns a list of all known upstart services, each of which is a hash ref
|
|
with 'name', 'desc', 'boot', 'status' and 'pid' keys. Also includes init.d
|
|
scripts, but if both exist then the native service will be preferred.
|
|
|
|
=cut
|
|
sub list_upstart_services
|
|
{
|
|
# Start with native upstart services
|
|
my @rv;
|
|
my $out = &backquote_command("initctl list");
|
|
my %done;
|
|
foreach my $l (split(/\r?\n/, $out)) {
|
|
if ($l =~ /^(\S+)\s+(start|stop)\/([a-z]+)/) {
|
|
my $s = { 'name' => $1,
|
|
'goal' => $2,
|
|
'status' => $3 };
|
|
if ($l =~ /process\s+(\d+)/) {
|
|
$s->{'pid'} = $1;
|
|
}
|
|
open(CONF, "</etc/init/$s->{'name'}.conf");
|
|
while(<CONF>) {
|
|
if (/^description\s+"([^"]+)"/ && !$s->{'desc'}) {
|
|
$s->{'desc'} = $1;
|
|
}
|
|
elsif (/^(#*)\s*start/ && !$s->{'boot'}) {
|
|
$s->{'boot'} = $1 ? 'stop' : 'start';
|
|
}
|
|
}
|
|
close(CONF);
|
|
push(@rv, $s);
|
|
$done{$s->{'name'}} = 1;
|
|
}
|
|
}
|
|
|
|
# Also add legacy init scripts
|
|
my @rls = &get_inittab_runlevel();
|
|
foreach my $a (&list_actions()) {
|
|
$a =~ s/\s+\d+$//;
|
|
next if ($done{$a});
|
|
my $f = &action_filename($a);
|
|
my $s = { 'name' => $a,
|
|
'legacy' => 1 };
|
|
$s->{'boot'} = 'stop';
|
|
foreach my $rl (@rls) {
|
|
my $l = glob("/etc/rc$rl.d/S*$a");
|
|
$s->{'boot'} = 'start' if ($l);
|
|
}
|
|
$s->{'desc'} = &init_description($f);
|
|
my $hasarg = &get_action_args($f);
|
|
if ($hasarg->{'status'}) {
|
|
my $r = &action_running($f);
|
|
if ($r == 0) {
|
|
$s->{'status'} = 'waiting';
|
|
}
|
|
elsif ($r == 1) {
|
|
$s->{'status'} = 'running';
|
|
}
|
|
}
|
|
push(@rv, $s);
|
|
}
|
|
|
|
return sort { $a->{'name'} cmp $b->{'name'} } @rv;
|
|
}
|
|
|
|
=head2 start_upstart_service(name)
|
|
|
|
Run the upstart service with some name, and return an OK flag and output
|
|
|
|
=cut
|
|
sub start_upstart_service
|
|
{
|
|
my ($name) = @_;
|
|
my $out = &backquote_logged(
|
|
"initctl start ".quotemeta($name)." 2>&1 </dev/null");
|
|
return (!$?, $out);
|
|
}
|
|
|
|
=head2 stop_upstart_service(name)
|
|
|
|
Shut down the upstart service with some name, and return an OK flag and output
|
|
|
|
=cut
|
|
sub stop_upstart_service
|
|
{
|
|
my ($name) = @_;
|
|
my $out = &backquote_logged(
|
|
"initctl stop ".quotemeta($name)." 2>&1 </dev/null");
|
|
return (!$?, $out);
|
|
}
|
|
|
|
=head2 restart_upstart_service(name)
|
|
|
|
Restart the upstart service with some name, and return an OK flag and output
|
|
|
|
=cut
|
|
sub restart_upstart_service
|
|
{
|
|
my ($name) = @_;
|
|
my $out = &backquote_logged(
|
|
"service ".quotemeta($name)." restart 2>&1 </dev/null");
|
|
return (!$?, $out);
|
|
}
|
|
|
|
=head2 reload_upstart_service(name)
|
|
|
|
Reload the upstart service with some name, and return an OK flag and output
|
|
|
|
=cut
|
|
sub reload_upstart_service
|
|
{
|
|
my ($name) = @_;
|
|
my $out = &backquote_logged(
|
|
"service ".quotemeta($name)." reload 2>&1 </dev/null");
|
|
return (!$?, $out);
|
|
}
|
|
|
|
=head2 create_upstart_service(name, description, command, [pre-script], [fork])
|
|
|
|
Create a new upstart service with the given details.
|
|
|
|
=cut
|
|
sub create_upstart_service
|
|
{
|
|
my ($name, $desc, $server, $prestart, $forks) = @_;
|
|
my $cfile = "/etc/init/$name.conf";
|
|
&open_lock_tempfile(CFILE, ">$cfile");
|
|
&print_tempfile(CFILE,
|
|
"# $name\n".
|
|
"#\n".
|
|
"# $desc\n".
|
|
"\n".
|
|
"description \"$desc\"\n".
|
|
"\n".
|
|
"start on runlevel [2345]\n".
|
|
"stop on runlevel [!2345]\n".
|
|
"\n"
|
|
);
|
|
if ($forks) {
|
|
&print_tempfile(CFILE,
|
|
"expect fork\n".
|
|
"\n"
|
|
);
|
|
}
|
|
if ($prestart) {
|
|
&print_tempfile(CFILE,
|
|
"pre-start script\n".
|
|
join("\n",
|
|
map { " ".$_."\n" }
|
|
split(/\n/, $prestart))."\n".
|
|
"end script\n".
|
|
"\n");
|
|
}
|
|
&print_tempfile(CFILE, "exec ".$server."\n");
|
|
&close_tempfile(CFILE);
|
|
}
|
|
|
|
=head2 delete_upstart_service(name)
|
|
|
|
Delete all traces of some upstart service
|
|
|
|
=cut
|
|
sub delete_upstart_service
|
|
{
|
|
my ($name) = @_;
|
|
if (&has_command("insserv")) {
|
|
&system_logged("insserv -r ".quotemeta($name)." >/dev/null 2>&1");
|
|
}
|
|
my $cfile = "/etc/init/$name.conf";
|
|
my $ifile = "/etc/init.d/$name";
|
|
&unlink_logged($cfile, $ifile);
|
|
}
|
|
|
|
=head2 list_systemd_services(skip-init)
|
|
|
|
Returns a list of all known systemd units, each of which is a hash ref
|
|
with 'name', 'desc', 'boot', 'status' and 'pid' keys. Also includes init.d
|
|
scripts, which will be preferred over native systemd units (because sometimes
|
|
systemd automatically includes init scripts).
|
|
|
|
=cut
|
|
sub list_systemd_services
|
|
{
|
|
my ($noinit) = @_;
|
|
if (@list_systemd_services_cache && !$noinit) {
|
|
return @list_systemd_services_cache;
|
|
}
|
|
|
|
my $units_piped = join('|', &get_systemd_unit_types());
|
|
my $creatable_piped = join('|', &get_systemd_creatable_unit_types());
|
|
my $list_piped = join('|', &get_systemd_list_unit_types());
|
|
my $list_types = join(" ", map { "-t ".quotemeta($_) }
|
|
&get_systemd_list_unit_types());
|
|
|
|
# Get all systemd unit names
|
|
my $out = &backquote_command("systemctl list-units --full --all $list_types --no-legend");
|
|
my $ex = $?;
|
|
foreach my $l (split(/\r?\n/, $out)) {
|
|
$l =~ s/^[^a-z0-9\-\_\.]+//i;
|
|
my ($unit, $loaded, $active, $sub, $desc) = split(/\s+/, $l, 5);
|
|
my $a = $unit;
|
|
$a =~ s/\.($units_piped)$//;
|
|
my $f = &action_filename($a);
|
|
if ($unit ne "UNIT" && $loaded eq "loaded" && !-r $f) {
|
|
push(@units, $unit);
|
|
}
|
|
}
|
|
&error("Failed to list systemd units : $out") if ($ex && @units < 10);
|
|
|
|
# Also find unit files for units that may be disabled at boot and not running,
|
|
# and so don't show up in systemctl list-units.
|
|
my $local_root = &get_systemd_root();
|
|
my $packaged_root = &get_systemd_root(undef, 1);
|
|
my @scan_roots = ( [ $local_root, $creatable_piped ] );
|
|
push(@scan_roots, [ $packaged_root, $list_piped ])
|
|
if ($packaged_root && $packaged_root ne $local_root);
|
|
foreach my $scan (@scan_roots) {
|
|
my ($root, $type_piped) = @$scan;
|
|
next if (!$root || !-d $root || !opendir(UNITS, $root));
|
|
push(@units, grep { !/\.wants$/ && !/^\./ && !-d "$root/$_" &&
|
|
/\.($type_piped)$/ } readdir(UNITS));
|
|
closedir(UNITS);
|
|
}
|
|
|
|
# Also add units from list-unit-files that also don't show up
|
|
$out = &backquote_command("systemctl list-unit-files $list_types --no-legend");
|
|
foreach my $l (split(/\r?\n/, $out)) {
|
|
if ($l =~ /^(\S+\.($units_piped))\s+disabled/ ||
|
|
$l =~ /^(\S+)\s+disabled/) {
|
|
push(@units, $1);
|
|
}
|
|
}
|
|
|
|
# Skip useless units
|
|
@units = grep { !/^sys-devices-/ &&
|
|
!/^\-\.mount/ &&
|
|
!/^\-\.slice/ &&
|
|
!/^dev-/ &&
|
|
!/^systemd-/ } @units;
|
|
@units = &unique(@units);
|
|
|
|
# Filter out templates
|
|
my @templates = grep { /\@$/ || /\@\.($units_piped)$/ } @units;
|
|
@units = grep { !/\@$/ && !/\@\.($units_piped)$/ } @units;
|
|
|
|
# Dump state of all of them, 100 at a time
|
|
my %info;
|
|
my $ecount = 0;
|
|
while(@units) {
|
|
my @args;
|
|
while(@args < 100 && @units) {
|
|
push(@args, shift(@units));
|
|
}
|
|
my $out = &backquote_command("systemctl show --property=Id,Description,UnitFileState,ActiveState,SubState,ExecStart,ExecStop,ExecReload,ExecMainPID,FragmentPath,DropInPaths ".join(" ", @args)." 2>/dev/null");
|
|
my @lines = split(/\r?\n/, $out);
|
|
my $curr;
|
|
my @units;
|
|
if (@lines) {
|
|
$curr = { };
|
|
push(@units, $curr);
|
|
}
|
|
foreach my $l (@lines) {
|
|
if ($l eq "") {
|
|
# Start of a new unit section
|
|
$curr = { };
|
|
push(@units, $curr);
|
|
}
|
|
else {
|
|
# A property in the current one
|
|
my ($n, $v) = split(/=/, $l, 2);
|
|
$curr->{$n} = $v;
|
|
}
|
|
}
|
|
foreach my $u (@units) {
|
|
$info{$u->{'Id'}} = $u if ($u->{'Id'});
|
|
}
|
|
$ecount++ if ($?);
|
|
}
|
|
if ($ecount && keys(%info) < 2) {
|
|
&error("Failed to read systemd units : <pre>$out</pre>");
|
|
}
|
|
|
|
# Extract info we want
|
|
my @rv;
|
|
my %done;
|
|
foreach my $name (keys %info) {
|
|
my $root = &get_systemd_root($name);
|
|
my $i = $info{$name};
|
|
next if ($i->{'Description'} =~ /^LSB:\s/);
|
|
push(@rv, { 'name' => $name,
|
|
'desc' => $i->{'Description'},
|
|
'legacy' => 0,
|
|
'boot' => $i->{'UnitFileState'} eq 'enabled' ? 1 :
|
|
$i->{'UnitFileState'} eq 'static' ? 2 :
|
|
$i->{'UnitFileState'} eq 'masked' ? -1 : 0,
|
|
'status' => $i->{'ActiveState'} eq 'active' ? 1 : 0,
|
|
'substatus' => $i->{'SubState'},
|
|
'fullstatus' => $i->{'SubState'} ?
|
|
"@{[ucfirst($i->{'ActiveState'})]} ($i->{'SubState'})" :
|
|
ucfirst($i->{'ActiveState'}),
|
|
'start' => $i->{'ExecStart'},
|
|
'stop' => $i->{'ExecStop'},
|
|
'reload' => $i->{'ExecReload'},
|
|
'pid' => $i->{'ExecMainPID'},
|
|
'file' => $i->{'FragmentPath'} || $root."/".$name,
|
|
});
|
|
$done{$name}++;
|
|
}
|
|
|
|
# Also add legacy init scripts
|
|
if (!$noinit) {
|
|
my @rls = &get_inittab_runlevel();
|
|
foreach my $a (&list_actions()) {
|
|
$a =~ s/\s+\d+$//;
|
|
next if ($done{$a} || $done{$a.".service"});
|
|
my $f = &action_filename($a);
|
|
my $s = { 'name' => $a,
|
|
'legacy' => 1 };
|
|
$s->{'boot'} = 0;
|
|
foreach my $rl (@rls) {
|
|
my $l = glob("/etc/rc$rl.d/S*$a");
|
|
$s->{'boot'} = 1 if ($l);
|
|
}
|
|
$s->{'desc'} = &init_description($f);
|
|
my $hasarg = &get_action_args($f);
|
|
if ($hasarg->{'status'}) {
|
|
my $r = &action_running($f);
|
|
$s->{'status'} = $r;
|
|
}
|
|
push(@rv, $s);
|
|
}
|
|
}
|
|
|
|
# Return actions sorted by name
|
|
@rv = sort { $a->{'name'} cmp $b->{'name'} } @rv;
|
|
@list_systemd_services_cache = @rv if (!$noinit);
|
|
return @rv;
|
|
}
|
|
|
|
=head2 start_systemd_service(name)
|
|
|
|
Run the systemd unit with some name, and return an OK flag and output.
|
|
|
|
=cut
|
|
sub start_systemd_service
|
|
{
|
|
my ($name) = @_;
|
|
my $out = &backquote_logged(
|
|
"systemctl start ".quotemeta($name)." 2>&1 </dev/null");
|
|
if ($? && $out =~ /journalctl/) {
|
|
$out .= &backquote_command("journalctl -xe 2>/dev/null");
|
|
}
|
|
return (!$?, $out);
|
|
}
|
|
|
|
=head2 stop_systemd_service(name)
|
|
|
|
Shut down the systemd unit with some name, and return an OK flag and output.
|
|
|
|
=cut
|
|
sub stop_systemd_service
|
|
{
|
|
my ($name) = @_;
|
|
my $out = &backquote_logged(
|
|
"systemctl stop ".quotemeta($name)." 2>&1 </dev/null");
|
|
return (!$?, $out);
|
|
}
|
|
|
|
=head2 restart_systemd_service(name)
|
|
|
|
Restart the systemd unit with some name, and return an OK flag and output.
|
|
|
|
=cut
|
|
sub restart_systemd_service
|
|
{
|
|
my ($name) = @_;
|
|
my $out = &backquote_logged(
|
|
"systemctl restart ".quotemeta($name)." 2>&1 </dev/null");
|
|
return (!$?, $out);
|
|
}
|
|
|
|
=head2 reload_systemd_service(name)
|
|
|
|
Reload the systemd unit with some name, and return an OK flag and output.
|
|
|
|
=cut
|
|
sub reload_systemd_service
|
|
{
|
|
my ($name) = @_;
|
|
my $out = &backquote_logged(
|
|
"systemctl reload ".quotemeta($name)." 2>&1 </dev/null");
|
|
return (!$?, $out);
|
|
}
|
|
|
|
=head2 status_systemd_service(name)
|
|
|
|
Get the full status of the systemd unit with some name.
|
|
|
|
=cut
|
|
sub status_systemd_service
|
|
{
|
|
my ($name) = @_;
|
|
my $out = &backquote_logged(
|
|
"systemctl --full --no-pager status ".quotemeta($name)." 2>&1 </dev/null");
|
|
return (!$?, $out);
|
|
}
|
|
|
|
=head2 logs_systemd_service(name)
|
|
|
|
Get recent journal logs for the systemd unit with some name.
|
|
|
|
=cut
|
|
sub logs_systemd_service
|
|
{
|
|
my ($name) = @_;
|
|
my $journalctl = &has_command("journalctl");
|
|
return (0, $text{'systemd_ejournal'}) if (!$journalctl);
|
|
my $out = &backquote_logged(
|
|
quotemeta($journalctl)." --no-pager --unit ".quotemeta($name).
|
|
" --lines 200 2>&1 </dev/null");
|
|
return (!$?, $out);
|
|
}
|
|
|
|
=head2 split_systemd_exec_commands(command)
|
|
|
|
Splits a multi-line systemd command field into individual command lines.
|
|
|
|
=cut
|
|
|
|
sub split_systemd_exec_commands
|
|
{
|
|
my ($cmd) = @_;
|
|
return ( ) if (!defined($cmd));
|
|
$cmd =~ s/\r//g;
|
|
my @rv;
|
|
foreach my $l (split(/\n/, $cmd)) {
|
|
$l =~ s/^\s+//;
|
|
$l =~ s/\s+$//;
|
|
push(@rv, $l) if ($l =~ /\S/);
|
|
}
|
|
return @rv;
|
|
}
|
|
|
|
=head2 systemd_shell_exec_command(shell, command)
|
|
|
|
Returns a systemd command line to run some command via a shell.
|
|
|
|
=cut
|
|
|
|
sub systemd_shell_exec_command
|
|
{
|
|
my ($sh, $cmd) = @_;
|
|
$cmd =~ s/'/'\\''/g;
|
|
return "$sh -c '$cmd'";
|
|
}
|
|
|
|
=head2 format_systemd_exec_command(shell, command)
|
|
|
|
Returns a systemd command line, using a shell if redirection is needed.
|
|
|
|
=cut
|
|
|
|
sub format_systemd_exec_command
|
|
{
|
|
my ($sh, $cmd) = @_;
|
|
return $cmd =~ /<|>/ ? &systemd_shell_exec_command($sh, $cmd) : $cmd;
|
|
}
|
|
|
|
=head2 clean_systemd_unit_value(value)
|
|
|
|
Returns a scalar systemd unit value with line breaks removed.
|
|
|
|
=cut
|
|
sub clean_systemd_unit_value
|
|
{
|
|
my ($value) = @_;
|
|
return if (!defined($value));
|
|
$value =~ s/\r|\n/ /g;
|
|
$value =~ s/\0//g;
|
|
$value =~ s/^\s+//;
|
|
$value =~ s/\s+$//;
|
|
return $value;
|
|
}
|
|
|
|
=head2 clean_systemd_unit_body(value)
|
|
|
|
Returns multi-line systemd unit directives with nulls and carriage returns
|
|
removed.
|
|
|
|
=cut
|
|
sub clean_systemd_unit_body
|
|
{
|
|
my ($value) = @_;
|
|
return if (!defined($value));
|
|
$value =~ s/\r//g;
|
|
$value =~ s/\0//g;
|
|
$value =~ s/^\s+//;
|
|
$value =~ s/\s+$//;
|
|
return $value;
|
|
}
|
|
|
|
=head2 quote_systemd_unit_word(value)
|
|
|
|
Returns a quoted systemd unit word with quotes and backslashes escaped.
|
|
|
|
=cut
|
|
sub quote_systemd_unit_word
|
|
{
|
|
my ($value) = @_;
|
|
$value =~ s/\\/\\\\/g;
|
|
$value =~ s/"/\\"/g;
|
|
return "\"$value\"";
|
|
}
|
|
|
|
=head2 format_systemd_environment_directives(value)
|
|
|
|
Returns Environment= lines for a user-entered set of environment variables.
|
|
|
|
=cut
|
|
sub format_systemd_environment_directives
|
|
{
|
|
my ($value) = @_;
|
|
$value = &clean_systemd_unit_value($value);
|
|
$value = "" if (!defined($value));
|
|
return ( ) if ($value !~ /\S/);
|
|
|
|
# Preserve quoted variable values while still allowing several NAME=VALUE
|
|
# words to become separate Environment= directives.
|
|
my @vars = &split_quoted_string($value);
|
|
@vars = ( $value ) if (!@vars);
|
|
return map { "Environment="."e_systemd_unit_word($_)."\n" } @vars;
|
|
}
|
|
|
|
=head2 format_systemd_output_value(value)
|
|
|
|
Returns a StandardOutput/StandardError value, appending to absolute files.
|
|
|
|
=cut
|
|
sub format_systemd_output_value
|
|
{
|
|
my ($value) = @_;
|
|
$value = &clean_systemd_unit_value($value);
|
|
$value = "" if (!defined($value));
|
|
return if ($value !~ /\S/);
|
|
return $value =~ /^\// ? "append:$value" : $value;
|
|
}
|
|
|
|
=head2 write_systemd_service_file(file, description, start-script, stop-script,
|
|
reload-script, [forks], [pidfile], [exits],
|
|
[&options])
|
|
|
|
Writes a systemd service file with the given details.
|
|
|
|
=cut
|
|
sub write_systemd_service_file
|
|
{
|
|
my ($cfile, $desc, $start, $stop, $restart, $forks, $pidfile, $exits, $opts) = @_;
|
|
my $sh = &has_command("sh") || "sh";
|
|
my $kill = &has_command("kill") || "kill";
|
|
$desc = &clean_systemd_unit_value($desc);
|
|
$pidfile = &clean_systemd_unit_value($pidfile);
|
|
|
|
# Scalar directives must stay on one line; command hook fields keep their
|
|
# line breaks so each line becomes a separate Exec*= directive.
|
|
if (ref($opts)) {
|
|
my %cleanopts;
|
|
foreach my $o (keys(%$opts)) {
|
|
$cleanopts{$o} = $o =~ /^(startpre|startpost|stoppost|stop|reload)$/ ?
|
|
$opts->{$o} : &clean_systemd_unit_value($opts->{$o});
|
|
}
|
|
$opts = \%cleanopts;
|
|
}
|
|
my @starts = &split_systemd_exec_commands($start);
|
|
my @stops = &split_systemd_exec_commands($stop);
|
|
my @restarts = &split_systemd_exec_commands($restart);
|
|
my $service_type = ref($opts) ? $opts->{'type'} : undef;
|
|
$service_type ||= $forks ? 'forking' : $exits ? 'oneshot' : undef;
|
|
|
|
# Multiple startup commands need oneshot semantics unless an explicit type was
|
|
# chosen. For other types, run them through one shell command.
|
|
my $multi_start_oneshot = @starts > 1 && !$service_type;
|
|
my $start_type = $service_type || ($multi_start_oneshot ? 'oneshot' : undef);
|
|
if (@starts > 1 && $start_type && $start_type ne 'oneshot') {
|
|
@starts = (&systemd_shell_exec_command($sh, join("; ", @starts)));
|
|
}
|
|
else {
|
|
@starts = map { &format_systemd_exec_command($sh, $_) } @starts;
|
|
}
|
|
@stops = map { &format_systemd_exec_command($sh, $_) } @stops;
|
|
@restarts = map { &format_systemd_exec_command($sh, $_) } @restarts;
|
|
my (@startpres, @startposts, @stopposts, @optstops, @optreloads);
|
|
if (ref($opts)) {
|
|
@startpres = map { &format_systemd_exec_command($sh, $_) }
|
|
&split_systemd_exec_commands($opts->{'startpre'});
|
|
@startposts = map { &format_systemd_exec_command($sh, $_) }
|
|
&split_systemd_exec_commands($opts->{'startpost'});
|
|
@stopposts = map { &format_systemd_exec_command($sh, $_) }
|
|
&split_systemd_exec_commands($opts->{'stoppost'});
|
|
@optstops = map { &format_systemd_exec_command($sh, $_) }
|
|
&split_systemd_exec_commands($opts->{'stop'})
|
|
if ($opts->{'stop'} && $opts->{'stop'} ne '0');
|
|
@optreloads = map { &format_systemd_exec_command($sh, $_) }
|
|
&split_systemd_exec_commands($opts->{'reload'})
|
|
if ($opts->{'reload'} && $opts->{'reload'} ne '0');
|
|
}
|
|
$service_type = 'oneshot' if ($multi_start_oneshot);
|
|
&open_lock_tempfile(CFILE, ">$cfile");
|
|
&print_tempfile(CFILE, "[Unit]\n");
|
|
&print_tempfile(CFILE, "Description=$desc\n") if ($desc);
|
|
if (ref($opts)) {
|
|
&print_tempfile(CFILE, "Before=$opts->{'before'}\n") if ($opts->{'before'});
|
|
&print_tempfile(CFILE, "After=$opts->{'after'}\n") if ($opts->{'after'});
|
|
&print_tempfile(CFILE, "Wants=$opts->{'wants'}\n") if ($opts->{'wants'});
|
|
&print_tempfile(CFILE, "Requires=$opts->{'requires'}\n") if ($opts->{'requires'});
|
|
&print_tempfile(CFILE, "Conflicts=$opts->{'conflicts'}\n") if ($opts->{'conflicts'});
|
|
&print_tempfile(CFILE, "OnFailure=$opts->{'onfailure'}\n") if ($opts->{'onfailure'});
|
|
&print_tempfile(CFILE, "OnSuccess=$opts->{'onsuccess'}\n") if ($opts->{'onsuccess'});
|
|
}
|
|
&print_tempfile(CFILE, "\n");
|
|
&print_tempfile(CFILE, "[Service]\n");
|
|
&print_tempfile(CFILE, "Type=$service_type\n") if ($service_type);
|
|
foreach my $startpre (@startpres) {
|
|
&print_tempfile(CFILE, "ExecStartPre=$startpre\n");
|
|
}
|
|
foreach my $start (@starts) {
|
|
&print_tempfile(CFILE, "ExecStart=$start\n");
|
|
}
|
|
foreach my $startpost (@startposts) {
|
|
&print_tempfile(CFILE, "ExecStartPost=$startpost\n");
|
|
}
|
|
foreach my $stop (@stops) {
|
|
&print_tempfile(CFILE, "ExecStop=$stop\n");
|
|
}
|
|
foreach my $stoppost (@stopposts) {
|
|
&print_tempfile(CFILE, "ExecStopPost=$stoppost\n");
|
|
}
|
|
foreach my $restart (@restarts) {
|
|
&print_tempfile(CFILE, "ExecReload=$restart\n");
|
|
}
|
|
&print_tempfile(CFILE, "RemainAfterExit=yes\n") if ($exits);
|
|
&print_tempfile(CFILE, "PIDFile=$pidfile\n") if ($pidfile);
|
|
|
|
# Optional [Service] directives from the advanced creation form.
|
|
if (ref($opts)) {
|
|
&print_tempfile(CFILE, "ExecStop=$kill \$MAINPID\n")
|
|
if (defined($opts->{'stop'}) && $opts->{'stop'} eq '0');
|
|
&print_tempfile(CFILE, "ExecReload=$kill -HUP \$MAINPID\n")
|
|
if (defined($opts->{'reload'}) && $opts->{'reload'} eq '0');
|
|
foreach my $stop (@optstops) {
|
|
&print_tempfile(CFILE, "ExecStop=$stop\n");
|
|
}
|
|
foreach my $reload (@optreloads) {
|
|
&print_tempfile(CFILE, "ExecReload=$reload\n");
|
|
}
|
|
foreach my $env (&format_systemd_environment_directives($opts->{'env'})) {
|
|
&print_tempfile(CFILE, $env);
|
|
}
|
|
&print_tempfile(CFILE, "EnvironmentFile=$opts->{'envfile'}\n") if ($opts->{'envfile'});
|
|
&print_tempfile(CFILE, "User=$opts->{'user'}\n") if ($opts->{'user'});
|
|
&print_tempfile(CFILE, "Group=$opts->{'group'}\n") if ($opts->{'group'});
|
|
&print_tempfile(CFILE, "KillMode=$opts->{'killmode'}\n") if ($opts->{'killmode'});
|
|
&print_tempfile(CFILE, "WorkingDirectory=$opts->{'workdir'}\n") if ($opts->{'workdir'});
|
|
&print_tempfile(CFILE, "Restart=$opts->{'restart'}\n") if ($opts->{'restart'});
|
|
&print_tempfile(CFILE, "RestartSec=$opts->{'restartsec'}\n") if ($opts->{'restartsec'});
|
|
&print_tempfile(CFILE, "WatchdogSec=$opts->{'watchdogsec'}\n") if ($opts->{'watchdogsec'});
|
|
my $timeoutstartsec = $opts->{'timeoutstartsec'} || $opts->{'timeout'};
|
|
&print_tempfile(CFILE, "TimeoutStartSec=$timeoutstartsec\n")
|
|
if ($timeoutstartsec);
|
|
&print_tempfile(CFILE, "TimeoutStopSec=$opts->{'timeoutstopsec'}\n") if ($opts->{'timeoutstopsec'});
|
|
&print_tempfile(CFILE, "LimitNOFILE=$opts->{'limitnofile'}\n") if ($opts->{'limitnofile'});
|
|
my $logout = &format_systemd_output_value($opts->{'logstd'});
|
|
my $logerr = &format_systemd_output_value($opts->{'logerr'});
|
|
&print_tempfile(CFILE, "StandardOutput=$logout\n") if ($logout);
|
|
&print_tempfile(CFILE, "StandardError=$logerr\n") if ($logerr);
|
|
&print_tempfile(CFILE, "SyslogIdentifier=$opts->{'syslogid'}\n") if ($opts->{'syslogid'});
|
|
&print_tempfile(CFILE, "NoNewPrivileges=yes\n") if ($opts->{'nonewprivs'});
|
|
&print_tempfile(CFILE, "PrivateTmp=yes\n") if ($opts->{'privatetmp'});
|
|
&print_tempfile(CFILE, "ProtectSystem=$opts->{'protectsystem'}\n") if ($opts->{'protectsystem'});
|
|
&print_tempfile(CFILE, "ReadWritePaths=$opts->{'readwritepaths'}\n") if ($opts->{'readwritepaths'});
|
|
}
|
|
|
|
&print_tempfile(CFILE, "\n");
|
|
&print_tempfile(CFILE, "[Install]\n");
|
|
if (ref($opts) && $opts->{'wantedby'}) {
|
|
&print_tempfile(CFILE, "WantedBy=$opts->{'wantedby'}\n");
|
|
}
|
|
else {
|
|
&print_tempfile(CFILE, "WantedBy=multi-user.target\n");
|
|
}
|
|
&close_tempfile(CFILE);
|
|
}
|
|
|
|
=head2 write_systemd_unit_file(file, type, description, body, [&options])
|
|
|
|
Writes a non-service systemd unit file with common [Unit] and [Install]
|
|
settings and a type-specific section.
|
|
|
|
=cut
|
|
sub write_systemd_unit_file
|
|
{
|
|
my ($cfile, $type, $desc, $body, $opts) = @_;
|
|
my $section = &get_systemd_unit_section($type);
|
|
$desc = &clean_systemd_unit_value($desc);
|
|
$body = &clean_systemd_unit_body($body);
|
|
$body = "" if (!defined($body));
|
|
if (ref($opts)) {
|
|
my %cleanopts;
|
|
foreach my $o (keys(%$opts)) {
|
|
$cleanopts{$o} = &clean_systemd_unit_value($opts->{$o});
|
|
}
|
|
$opts = \%cleanopts;
|
|
}
|
|
|
|
&open_lock_tempfile(CFILE, ">$cfile");
|
|
&print_tempfile(CFILE, "[Unit]\n");
|
|
&print_tempfile(CFILE, "Description=$desc\n") if ($desc);
|
|
|
|
# Relationship options are common [Unit] directives for all unit types.
|
|
if (ref($opts)) {
|
|
&print_tempfile(CFILE, "Before=$opts->{'before'}\n") if ($opts->{'before'});
|
|
&print_tempfile(CFILE, "After=$opts->{'after'}\n") if ($opts->{'after'});
|
|
&print_tempfile(CFILE, "Wants=$opts->{'wants'}\n") if ($opts->{'wants'});
|
|
&print_tempfile(CFILE, "Requires=$opts->{'requires'}\n") if ($opts->{'requires'});
|
|
&print_tempfile(CFILE, "Conflicts=$opts->{'conflicts'}\n") if ($opts->{'conflicts'});
|
|
&print_tempfile(CFILE, "OnFailure=$opts->{'onfailure'}\n") if ($opts->{'onfailure'});
|
|
&print_tempfile(CFILE, "OnSuccess=$opts->{'onsuccess'}\n") if ($opts->{'onsuccess'});
|
|
}
|
|
|
|
# The UI accepts only the body of the type-specific section; wrap it here so
|
|
# users do not need to type [Timer], [Socket], and so on.
|
|
if ($section && $body =~ /\S/) {
|
|
&print_tempfile(CFILE, "\n[$section]\n");
|
|
&print_tempfile(CFILE, $body);
|
|
&print_tempfile(CFILE, "\n") if ($body !~ /\n$/);
|
|
}
|
|
|
|
if (ref($opts) && $opts->{'wantedby'}) {
|
|
&print_tempfile(CFILE, "\n[Install]\n");
|
|
&print_tempfile(CFILE, "WantedBy=$opts->{'wantedby'}\n");
|
|
}
|
|
&close_tempfile(CFILE);
|
|
}
|
|
|
|
=head2 create_systemd_service(name, description, start-script, stop-script,
|
|
reload-script, [forks], [pidfile], [exits],
|
|
[&options])
|
|
|
|
Create a new systemd service with the given details.
|
|
|
|
=cut
|
|
sub create_systemd_service
|
|
{
|
|
my ($name, $desc, $start, $stop, $restart, $forks, $pidfile, $exits, $opts) = @_;
|
|
my $cfile = &get_systemd_root($name)."/".$name;
|
|
&write_systemd_service_file($cfile, $desc, $start, $stop, $restart, $forks,
|
|
$pidfile, $exits, $opts);
|
|
&restart_systemd();
|
|
}
|
|
|
|
=head2 create_systemd_unit(name, type, description, body, [&options])
|
|
|
|
Create a new non-service systemd unit with the given details.
|
|
|
|
=cut
|
|
sub create_systemd_unit
|
|
{
|
|
my ($name, $type, $desc, $body, $opts) = @_;
|
|
my $cfile = &get_systemd_root($name)."/".$name;
|
|
&write_systemd_unit_file($cfile, $type, $desc, $body, $opts);
|
|
&restart_systemd();
|
|
}
|
|
|
|
=head2 get_systemd_user_details(user)
|
|
|
|
Returns user account details needed for per-user systemd units.
|
|
|
|
=cut
|
|
sub get_systemd_user_details
|
|
{
|
|
my ($user) = @_;
|
|
return if (!$user || $user =~ /[\0\r\n\/]/);
|
|
my @uinfo = getpwnam($user);
|
|
return if (!@uinfo || $uinfo[7] !~ /^\//);
|
|
return { 'user' => $uinfo[0],
|
|
'uid' => $uinfo[2],
|
|
'gid' => $uinfo[3],
|
|
'home' => $uinfo[7] };
|
|
}
|
|
|
|
=head2 get_systemd_user_root(user)
|
|
|
|
Returns the base directory for a user's systemd unit config files.
|
|
|
|
=cut
|
|
sub get_systemd_user_root
|
|
{
|
|
my ($user) = @_;
|
|
my $uinfo = &get_systemd_user_details($user);
|
|
return if (!$uinfo);
|
|
return $uinfo->{'home'}."/.config/systemd/user";
|
|
}
|
|
|
|
=head2 valid_systemd_unit_name(name)
|
|
|
|
Returns 1 if a systemd unit name is safe for direct file management.
|
|
|
|
=cut
|
|
sub valid_systemd_unit_name
|
|
{
|
|
my ($name) = @_;
|
|
my $units_piped = join('|', &get_systemd_creatable_unit_types());
|
|
return 0 if ($name =~ /\@$/ || $name =~ /\@\.($units_piped)$/i);
|
|
return $name && $name =~ /^[a-z0-9\.\_\-\@:]+\.($units_piped)$/i;
|
|
}
|
|
|
|
=head2 systemd_user_root_safe(user)
|
|
|
|
Returns 1 if the user's systemd unit config path does not contain symlinked
|
|
components controlled by the user.
|
|
|
|
=cut
|
|
sub systemd_user_root_safe
|
|
{
|
|
my ($user) = @_;
|
|
my $uinfo = &get_systemd_user_details($user);
|
|
return 0 if (!$uinfo);
|
|
|
|
# Every path component below ~/.config is user-controlled, so reject symlinks
|
|
# before any root-visible operation uses the directory.
|
|
foreach my $dir ($uinfo->{'home'}."/.config",
|
|
$uinfo->{'home'}."/.config/systemd",
|
|
$uinfo->{'home'}."/.config/systemd/user") {
|
|
return 0 if (-l $dir);
|
|
return 0 if (-e $dir && !-d $dir);
|
|
}
|
|
return 1;
|
|
}
|
|
|
|
=head2 systemd_user_unit_file_safe(user, file, [must-exist])
|
|
|
|
Returns 1 if a user unit file is a direct, non-symlinked file below the
|
|
user's systemd unit config directory.
|
|
|
|
=cut
|
|
sub systemd_user_unit_file_safe
|
|
{
|
|
my ($user, $file, $must_exist) = @_;
|
|
my $root = &get_systemd_user_root($user);
|
|
return 0 if (!$root || !$file || $file =~ /[\0\r\n]/);
|
|
return 0 if (!&systemd_user_root_safe($user));
|
|
|
|
# Only direct child unit files are managed. This prevents path traversal and
|
|
# avoids following user-created subdirectories or symlinks.
|
|
return 0 if ($file !~ /^\Q$root\E\/([^\/]+)$/);
|
|
my $unit = $1;
|
|
return 0 if (!&valid_systemd_unit_name($unit));
|
|
return 0 if (-l $file);
|
|
return $must_exist ? -f $file : (!-e $file || -f $file);
|
|
}
|
|
|
|
=head2 read_systemd_user_unit_file(user, file)
|
|
|
|
Reads a user unit file as the owning Unix user after path validation.
|
|
|
|
=cut
|
|
sub read_systemd_user_unit_file
|
|
{
|
|
my ($user, $file) = @_;
|
|
return if (!&systemd_user_unit_file_safe($user, $file, 1));
|
|
return &eval_as_unix_user($user, sub {
|
|
return &read_file_contents($file);
|
|
});
|
|
}
|
|
|
|
=head2 write_systemd_user_unit_file(user, file, data)
|
|
|
|
Writes a user unit file as the owning Unix user after path validation.
|
|
|
|
=cut
|
|
sub write_systemd_user_unit_file
|
|
{
|
|
my ($user, $file, $data) = @_;
|
|
return (0, $text{'systemd_euserunitfile'})
|
|
if (!&systemd_user_unit_file_safe($user, $file, 0));
|
|
return (1, undef) if (&is_readonly_mode());
|
|
my $ok = eval {
|
|
# Drop privileges for the actual write so a race cannot make root write
|
|
# through a user-controlled symlink.
|
|
&eval_as_unix_user($user, sub {
|
|
&open_lock_tempfile(USERUNIT, ">$file");
|
|
&print_tempfile(USERUNIT, $data);
|
|
&close_tempfile(USERUNIT);
|
|
&set_ownership_permissions(undef, undef, 0644, $file);
|
|
});
|
|
1;
|
|
};
|
|
my $err = $@;
|
|
$err =~ s/\s+at\s+(\/\S+)\s+line\s+(\d+)\.?// if ($err);
|
|
return $ok ? (1, undef) : (0, $err || $text{'systemd_euserunitfile'});
|
|
}
|
|
|
|
=head2 delete_systemd_user_unit_file(user, file)
|
|
|
|
Deletes a user unit file as the owning Unix user after path validation, so a
|
|
symlinked path component cannot trick root into removing files outside the
|
|
user's systemd unit config directory.
|
|
|
|
=cut
|
|
sub delete_systemd_user_unit_file
|
|
{
|
|
my ($user, $file) = @_;
|
|
return 0 if (!&systemd_user_unit_file_safe($user, $file, 0));
|
|
return 1 if (&is_readonly_mode());
|
|
return &eval_as_unix_user($user, sub {
|
|
# Re-check in the user's context immediately before unlinking.
|
|
return 1 if (!-e $file && !-l $file);
|
|
return 0 if (-l $file || !-f $file);
|
|
return &unlink_file($file) ? 1 : 0;
|
|
});
|
|
}
|
|
|
|
=head2 make_systemd_user_root(user)
|
|
|
|
Creates the base directory for a user's systemd unit config files.
|
|
|
|
=cut
|
|
sub make_systemd_user_root
|
|
{
|
|
my ($user) = @_;
|
|
my $uinfo = &get_systemd_user_details($user);
|
|
return if (!$uinfo);
|
|
my @dirs = ( $uinfo->{'home'}."/.config",
|
|
$uinfo->{'home'}."/.config/systemd",
|
|
$uinfo->{'home'}."/.config/systemd/user" );
|
|
foreach my $dir (@dirs) {
|
|
return if (-l $dir || (-e $dir && !-d $dir));
|
|
}
|
|
return $dirs[-1] if (&is_readonly_mode() && &systemd_user_root_safe($user));
|
|
my $ok = eval {
|
|
# Create the directory tree as the owning user, then validate it again in
|
|
# root context before returning the path.
|
|
&eval_as_unix_user($uinfo->{'user'}, sub {
|
|
foreach my $dir (@dirs) {
|
|
return 0 if (-l $dir || (-e $dir && !-d $dir));
|
|
if (!-d $dir) {
|
|
&make_dir($dir, 0755, 0) || return 0;
|
|
}
|
|
}
|
|
return 1;
|
|
});
|
|
};
|
|
return if (!$ok || !&systemd_user_root_safe($user));
|
|
return $dirs[-1];
|
|
}
|
|
|
|
=head2 systemd_user_command(user, command, ...)
|
|
|
|
Returns a command line that runs a command inside the user's systemd context.
|
|
|
|
=cut
|
|
sub systemd_user_command
|
|
{
|
|
my ($user, @cmd) = @_;
|
|
my $uinfo = &get_systemd_user_details($user);
|
|
return if (!$uinfo);
|
|
my $runtime = "/run/user/".$uinfo->{'uid'};
|
|
|
|
# systemctl --user needs the user's home, runtime directory and bus address
|
|
# even though the CGI is running from Webmin's root-owned environment.
|
|
my $env = "HOME=".quotemeta($uinfo->{'home'})." ".
|
|
"XDG_RUNTIME_DIR=".quotemeta($runtime)." ".
|
|
"DBUS_SESSION_BUS_ADDRESS=".quotemeta("unix:path=".$runtime."/bus");
|
|
return &command_as_user($uinfo->{'user'}, 0, $env." ".join(" ", @cmd));
|
|
}
|
|
|
|
=head2 systemd_user_systemctl_command(user, arg, ...)
|
|
|
|
Returns a systemctl --user command line for some user.
|
|
|
|
=cut
|
|
sub systemd_user_systemctl_command
|
|
{
|
|
my ($user, @args) = @_;
|
|
my $systemctl = &has_command("systemctl") || "systemctl";
|
|
return &systemd_user_command($user, quotemeta($systemctl), "--user",
|
|
map { quotemeta($_) } @args);
|
|
}
|
|
|
|
=head2 run_systemd_user_systemctl(user, arg, ...)
|
|
|
|
Runs systemctl --user for some user, returning an OK flag and output.
|
|
|
|
=cut
|
|
sub run_systemd_user_systemctl
|
|
{
|
|
my ($user, @args) = @_;
|
|
my $cmd = &systemd_user_systemctl_command($user, @args);
|
|
return (0, $text{'systemd_euser'}) if (!$cmd);
|
|
my $out = &backquote_logged($cmd." 2>&1 </dev/null");
|
|
return (!$?, $out);
|
|
}
|
|
|
|
=head2 restart_systemd_user(user)
|
|
|
|
Tell a user's systemd daemon to re-read its config.
|
|
|
|
=cut
|
|
sub restart_systemd_user
|
|
{
|
|
my ($user) = @_;
|
|
return &run_systemd_user_systemctl($user, "daemon-reload");
|
|
}
|
|
|
|
=head2 set_systemd_user_linger(user, enabled)
|
|
|
|
Enables or disables lingering for a user.
|
|
|
|
=cut
|
|
sub set_systemd_user_linger
|
|
{
|
|
my ($user, $enabled) = @_;
|
|
my $uinfo = &get_systemd_user_details($user);
|
|
return (0, $text{'systemd_euser'}) if (!$uinfo);
|
|
my $loginctl = &has_command("loginctl");
|
|
return (0, $text{'systemd_eloginctl'}) if (!$loginctl);
|
|
my $cmd = quotemeta($loginctl)." ".
|
|
($enabled ? "enable-linger" : "disable-linger")." ".
|
|
quotemeta($uinfo->{'user'});
|
|
my $out = &backquote_logged($cmd." 2>&1 </dev/null");
|
|
return (!$?, $out);
|
|
}
|
|
|
|
=head2 systemd_user_linger_enabled(user)
|
|
|
|
Returns 1 if lingering is enabled for some user, 0 if not.
|
|
|
|
=cut
|
|
sub systemd_user_linger_enabled
|
|
{
|
|
my ($user) = @_;
|
|
my $uinfo = &get_systemd_user_details($user);
|
|
return 0 if (!$uinfo);
|
|
return 1 if (-e "/var/lib/systemd/linger/".$uinfo->{'user'});
|
|
my $loginctl = &has_command("loginctl");
|
|
return 0 if (!$loginctl);
|
|
my $out = &backquote_command(quotemeta($loginctl)." show-user ".
|
|
quotemeta($uinfo->{'user'}).
|
|
" -p Linger 2>/dev/null");
|
|
return $out =~ /^Linger=yes/m ? 1 : 0;
|
|
}
|
|
|
|
=head2 start_systemd_user_manager(user)
|
|
|
|
Starts a user's systemd manager through the system manager.
|
|
|
|
=cut
|
|
sub start_systemd_user_manager
|
|
{
|
|
my ($user) = @_;
|
|
my $uinfo = &get_systemd_user_details($user);
|
|
return (0, $text{'systemd_euser'}) if (!$uinfo);
|
|
my $systemctl = &has_command("systemctl") || "systemctl";
|
|
my $unit = "user\@".$uinfo->{'uid'}.".service";
|
|
my $out = &backquote_logged(quotemeta($systemctl)." start ".
|
|
quotemeta($unit)." 2>&1 </dev/null");
|
|
return (!$?, $out);
|
|
}
|
|
|
|
=head2 systemd_user_file_description(user, file)
|
|
|
|
Returns the Description= line from a systemd unit file.
|
|
|
|
=cut
|
|
sub systemd_user_file_description
|
|
{
|
|
my ($user, $file) = @_;
|
|
my $data = &read_systemd_user_unit_file($user, $file);
|
|
return if (!defined($data));
|
|
return $1 if ($data =~ /^Description=(.*)$/m);
|
|
return;
|
|
}
|
|
|
|
=head2 systemd_user_file_enabled(user, name)
|
|
|
|
Returns 1 if some user unit file is enabled by a *.wants symlink. The lookup
|
|
runs as the owning Unix user so that a symlinked path component cannot make
|
|
root probe files outside the user's systemd unit config directory.
|
|
|
|
=cut
|
|
sub systemd_user_file_enabled
|
|
{
|
|
my ($user, $name) = @_;
|
|
my $root = &get_systemd_user_root($user);
|
|
return 0 if (!$root || !&systemd_user_root_safe($user));
|
|
return &eval_as_unix_user($user, sub {
|
|
return 0 if (!-d $root);
|
|
opendir(my $dh, $root) || return 0;
|
|
my @dirs = grep { /\.wants$/ && -d "$root/$_" } readdir($dh);
|
|
closedir($dh);
|
|
foreach my $dir (@dirs) {
|
|
return 1 if (-e "$root/$dir/$name");
|
|
}
|
|
return 0;
|
|
});
|
|
}
|
|
|
|
=head2 list_systemd_user_services(user)
|
|
|
|
Returns a list of all known systemd user units for some user.
|
|
|
|
=cut
|
|
sub list_systemd_user_services
|
|
{
|
|
my ($user) = @_;
|
|
my $uinfo = &get_systemd_user_details($user);
|
|
return ( ) if (!$uinfo);
|
|
my $units_piped = join('|', &get_systemd_unit_types());
|
|
my $list_types = join(" ", map { "-t ".quotemeta($_) }
|
|
&get_systemd_list_unit_types());
|
|
my @units;
|
|
my $root = &get_systemd_user_root($user);
|
|
my %local_files;
|
|
|
|
# Read local user unit files even if the user's manager is not running.
|
|
if ($root && &systemd_user_root_safe($user)) {
|
|
my @local_units = &eval_as_unix_user($user, sub {
|
|
return ( ) if (!-d $root || !opendir(SYSTEMDUSERUNITS, $root));
|
|
my @rv;
|
|
foreach my $unit (readdir(SYSTEMDUSERUNITS)) {
|
|
next if ($unit =~ /^\./ || $unit =~ /\.wants$/ ||
|
|
$unit !~ /\.($units_piped)$/ || -d "$root/$unit");
|
|
push(@rv, $unit);
|
|
}
|
|
closedir(SYSTEMDUSERUNITS);
|
|
return @rv;
|
|
});
|
|
foreach my $unit (@local_units) {
|
|
next if (!&systemd_user_unit_file_safe($user, "$root/$unit", 1));
|
|
push(@units, $unit);
|
|
$local_files{$unit} = "$root/$unit";
|
|
}
|
|
}
|
|
|
|
# Add active or loaded units from the user's manager.
|
|
my $out = &backquote_command(
|
|
&systemd_user_systemctl_command($user, "list-units", "--full",
|
|
"--all", split(/\s+/, $list_types),
|
|
"--no-legend")." 2>/dev/null");
|
|
foreach my $l (split(/\r?\n/, $out)) {
|
|
$l =~ s/^[^a-z0-9\-\_\.]+//i;
|
|
my ($unit, $loaded) = split(/\s+/, $l, 3);
|
|
push(@units, $unit)
|
|
if ($unit && $unit ne "UNIT" && $loaded eq "loaded");
|
|
}
|
|
|
|
# Also add units from list-unit-files that may not be loaded.
|
|
$out = &backquote_command(
|
|
&systemd_user_systemctl_command($user, "list-unit-files",
|
|
split(/\s+/, $list_types),
|
|
"--no-legend").
|
|
" 2>/dev/null");
|
|
foreach my $l (split(/\r?\n/, $out)) {
|
|
if ($l =~ /^(\S+)\s+/) {
|
|
push(@units, $1);
|
|
}
|
|
}
|
|
|
|
@units = grep { !/\@$/ && !/\@\.($units_piped)$/ } &unique(@units);
|
|
|
|
# Dump state in batches, keeping command lines short and parsing the property
|
|
# format into one hash per unit.
|
|
my @show_units = @units;
|
|
my %info;
|
|
while(@show_units) {
|
|
my @args;
|
|
while(@args < 100 && @show_units) {
|
|
push(@args, shift(@show_units));
|
|
}
|
|
my $cmd = &systemd_user_systemctl_command(
|
|
$user, "show",
|
|
"--property=Id,Description,UnitFileState,ActiveState,SubState,ExecStart,ExecStop,ExecReload,ExecMainPID,FragmentPath,DropInPaths",
|
|
@args);
|
|
my $show = &backquote_command($cmd." 2>/dev/null");
|
|
my @lines = split(/\r?\n/, $show);
|
|
my $curr;
|
|
my @shown;
|
|
if (@lines) {
|
|
$curr = { };
|
|
push(@shown, $curr);
|
|
}
|
|
foreach my $l (@lines) {
|
|
if ($l eq "") {
|
|
$curr = { };
|
|
push(@shown, $curr);
|
|
}
|
|
else {
|
|
my ($n, $v) = split(/=/, $l, 2);
|
|
$curr->{$n} = $v;
|
|
}
|
|
}
|
|
foreach my $u (@shown) {
|
|
$info{$u->{'Id'}} = $u if ($u->{'Id'});
|
|
}
|
|
}
|
|
|
|
my @rv;
|
|
my %done;
|
|
foreach my $name (sort keys %info) {
|
|
my $i = $info{$name};
|
|
my $file = $i->{'FragmentPath'} || $local_files{$name};
|
|
|
|
# Only expose local user-owned files. Vendor user units are not editable
|
|
# here because deletion/editing would not affect their source.
|
|
next if ($root && (!$file || $file !~ /^\Q$root\E\//));
|
|
next if (!&systemd_user_unit_file_safe($user, $file, 1));
|
|
next if ($i->{'Description'} =~ /^LSB:\s/);
|
|
push(@rv, { 'name' => $name,
|
|
'desc' => $i->{'Description'},
|
|
'legacy' => 0,
|
|
'boot' => $i->{'UnitFileState'} =~ /^enabled/ ? 1 :
|
|
$i->{'UnitFileState'} eq 'static' ? 2 :
|
|
$i->{'UnitFileState'} eq 'masked' ? -1 : 0,
|
|
'status' => $i->{'ActiveState'} eq 'active' ? 1 : 0,
|
|
'substatus' => $i->{'SubState'},
|
|
'fullstatus' => $i->{'SubState'} ?
|
|
"@{[ucfirst($i->{'ActiveState'})]} ($i->{'SubState'})" :
|
|
ucfirst($i->{'ActiveState'}),
|
|
'start' => $i->{'ExecStart'},
|
|
'stop' => $i->{'ExecStop'},
|
|
'reload' => $i->{'ExecReload'},
|
|
'pid' => $i->{'ExecMainPID'},
|
|
'file' => $file,
|
|
'user' => $uinfo->{'user'},
|
|
});
|
|
$done{$name}++;
|
|
}
|
|
|
|
foreach my $name (sort keys %local_files) {
|
|
next if ($done{$name});
|
|
|
|
# Include local files even when the user manager is offline and cannot
|
|
# report them through systemctl show.
|
|
push(@rv, { 'name' => $name,
|
|
'desc' => &systemd_user_file_description(
|
|
$user, $local_files{$name}) || "",
|
|
'legacy' => 0,
|
|
'boot' => &systemd_user_file_enabled($user, $name),
|
|
'status' => undef,
|
|
'fullstatus' => undef,
|
|
'file' => $local_files{$name},
|
|
'user' => $uinfo->{'user'},
|
|
});
|
|
}
|
|
|
|
return sort { $a->{'name'} cmp $b->{'name'} } @rv;
|
|
}
|
|
|
|
=head2 list_all_systemd_user_services()
|
|
|
|
Returns all locally editable systemd user units from users' home directories.
|
|
|
|
=cut
|
|
sub list_all_systemd_user_services
|
|
{
|
|
my @rv;
|
|
setpwent();
|
|
while(my @uinfo = getpwent()) {
|
|
my ($user, $home) = ($uinfo[0], $uinfo[7]);
|
|
next if (!$user || $home !~ /^\//);
|
|
my $root = $home."/.config/systemd/user";
|
|
next if (!-d $root || !&systemd_user_root_safe($user));
|
|
push(@rv, &list_systemd_user_services($user));
|
|
}
|
|
endpwent();
|
|
return sort { $a->{'user'} cmp $b->{'user'} ||
|
|
$a->{'name'} cmp $b->{'name'} } @rv;
|
|
}
|
|
|
|
=head2 start_systemd_user_service(user, name)
|
|
|
|
Run the systemd user unit with some name, and return OK flag and output.
|
|
|
|
=cut
|
|
sub start_systemd_user_service
|
|
{
|
|
my ($user, $name) = @_;
|
|
my ($ok, $out) = &run_systemd_user_systemctl($user, "start", $name);
|
|
if (!$ok && $out =~ /journalctl/) {
|
|
my ($lok, $lout) = &logs_systemd_user_service($user, $name);
|
|
$out .= $lout if ($lout);
|
|
}
|
|
return ($ok, $out);
|
|
}
|
|
|
|
=head2 stop_systemd_user_service(user, name)
|
|
|
|
Shut down the systemd user unit with some name.
|
|
|
|
=cut
|
|
sub stop_systemd_user_service
|
|
{
|
|
my ($user, $name) = @_;
|
|
return &run_systemd_user_systemctl($user, "stop", $name);
|
|
}
|
|
|
|
=head2 restart_systemd_user_service(user, name)
|
|
|
|
Restart the systemd user unit with some name.
|
|
|
|
=cut
|
|
sub restart_systemd_user_service
|
|
{
|
|
my ($user, $name) = @_;
|
|
return &run_systemd_user_systemctl($user, "restart", $name);
|
|
}
|
|
|
|
=head2 status_systemd_user_service(user, name)
|
|
|
|
Get the full status of the systemd user unit with some name.
|
|
|
|
=cut
|
|
sub status_systemd_user_service
|
|
{
|
|
my ($user, $name) = @_;
|
|
return &run_systemd_user_systemctl($user, "--full", "--no-pager",
|
|
"status", $name);
|
|
}
|
|
|
|
=head2 logs_systemd_user_service(user, name)
|
|
|
|
Get recent journal logs for the systemd user unit with some name.
|
|
|
|
=cut
|
|
sub logs_systemd_user_service
|
|
{
|
|
my ($user, $name) = @_;
|
|
my $journalctl = &has_command("journalctl");
|
|
return (0, $text{'systemd_ejournal'}) if (!$journalctl);
|
|
my $cmd = &systemd_user_command($user, quotemeta($journalctl), "--user",
|
|
"--no-pager", "--unit", quotemeta($name),
|
|
"--lines", "200");
|
|
return (0, $text{'systemd_euser'}) if (!$cmd);
|
|
my $out = &backquote_logged($cmd." 2>&1 </dev/null");
|
|
return (!$?, $out);
|
|
}
|
|
|
|
=head2 enable_systemd_user_service(user, name)
|
|
|
|
Enable a systemd user unit.
|
|
|
|
=cut
|
|
sub enable_systemd_user_service
|
|
{
|
|
my ($user, $name) = @_;
|
|
return &run_systemd_user_systemctl($user, "enable", $name);
|
|
}
|
|
|
|
=head2 disable_systemd_user_service(user, name)
|
|
|
|
Disable a systemd user unit.
|
|
|
|
=cut
|
|
sub disable_systemd_user_service
|
|
{
|
|
my ($user, $name) = @_;
|
|
return &run_systemd_user_systemctl($user, "disable", $name);
|
|
}
|
|
|
|
=head2 create_systemd_user_service(user, name, description, start-script, stop-script,
|
|
reload-script, [forks], [pidfile], [exits],
|
|
[&options])
|
|
|
|
Create a new systemd user service with the given details.
|
|
|
|
=cut
|
|
sub create_systemd_user_service
|
|
{
|
|
my ($user, $name, $desc, $start, $stop, $restart, $forks, $pidfile, $exits,
|
|
$opts) = @_;
|
|
my $uinfo = &get_systemd_user_details($user);
|
|
return (0, $text{'systemd_euser'}) if (!$uinfo);
|
|
my $root = &make_systemd_user_root($user);
|
|
return (0, $text{'systemd_euserhome'}) if (!$root);
|
|
my $cfile = $root."/".$name;
|
|
return (0, $text{'systemd_eclash'}) if (-e $cfile || -l $cfile);
|
|
|
|
# Render with the system service writer, then copy the bytes into the user's
|
|
# home through the privilege-dropped user-unit writer.
|
|
my $tmp = &transname();
|
|
&write_systemd_service_file($tmp, $desc, $start, $stop, $restart, $forks,
|
|
$pidfile, $exits, $opts);
|
|
my $data = &read_file_contents($tmp);
|
|
&unlink_file($tmp);
|
|
my ($wok, $wout) = &write_systemd_user_unit_file($user, $cfile, $data);
|
|
return ($wok, $wout) if (!$wok);
|
|
my ($ok, $out) = &restart_systemd_user($user);
|
|
if (!$ok) {
|
|
# Avoid leaving a half-created unit when daemon-reload cannot see it.
|
|
&delete_systemd_user_unit_file($user, $cfile);
|
|
}
|
|
return ($ok, $out);
|
|
}
|
|
|
|
=head2 create_systemd_user_unit(user, name, type, description, body, [&options])
|
|
|
|
Create a new non-service systemd user unit with the given details.
|
|
|
|
=cut
|
|
sub create_systemd_user_unit
|
|
{
|
|
my ($user, $name, $type, $desc, $body, $opts) = @_;
|
|
my $uinfo = &get_systemd_user_details($user);
|
|
return (0, $text{'systemd_euser'}) if (!$uinfo);
|
|
my $root = &make_systemd_user_root($user);
|
|
return (0, $text{'systemd_euserhome'}) if (!$root);
|
|
my $cfile = $root."/".$name;
|
|
return (0, $text{'systemd_eclash'}) if (-e $cfile || -l $cfile);
|
|
|
|
# Non-service units share the same safe user write and rollback path.
|
|
my $tmp = &transname();
|
|
&write_systemd_unit_file($tmp, $type, $desc, $body, $opts);
|
|
my $data = &read_file_contents($tmp);
|
|
&unlink_file($tmp);
|
|
my ($wok, $wout) = &write_systemd_user_unit_file($user, $cfile, $data);
|
|
return ($wok, $wout) if (!$wok);
|
|
my ($ok, $out) = &restart_systemd_user($user);
|
|
if (!$ok) {
|
|
# Avoid leaving a half-created unit when daemon-reload cannot see it.
|
|
&delete_systemd_user_unit_file($user, $cfile);
|
|
}
|
|
return ($ok, $out);
|
|
}
|
|
|
|
=head2 delete_systemd_user_service(user, name)
|
|
|
|
Delete all traces of some systemd user unit.
|
|
|
|
=cut
|
|
sub delete_systemd_user_service
|
|
{
|
|
my ($user, $name) = @_;
|
|
my $root = &get_systemd_user_root($user);
|
|
return (0, $text{'systemd_euser'}) if (!$root);
|
|
my $unit = &action_unit($name);
|
|
|
|
# Keep the old bare-name compatibility path, but every candidate is deleted
|
|
# through the privilege-dropped helper.
|
|
foreach my $file (map { $root."/".$_ } &unique($name, $unit)) {
|
|
&delete_systemd_user_unit_file($user, $file) ||
|
|
return (0, $text{'systemd_euserunitfile'});
|
|
}
|
|
return &restart_systemd_user($user);
|
|
}
|
|
|
|
=head2 delete_systemd_service(name)
|
|
|
|
Delete all traces of some systemd unit.
|
|
|
|
=cut
|
|
sub delete_systemd_service
|
|
{
|
|
my ($name) = @_;
|
|
my $unit = &action_unit($name);
|
|
foreach my $file (&unique($name, $unit)) {
|
|
&unlink_logged(&get_systemd_root($file)."/".$file);
|
|
}
|
|
&restart_systemd();
|
|
}
|
|
|
|
=head2 get_systemd_unit_types()
|
|
|
|
Returns a list of all systemd unit types. Returns a string
|
|
instead if separator param is set.
|
|
|
|
=cut
|
|
sub get_systemd_unit_types
|
|
{
|
|
return ('target', 'service', 'socket', 'device', 'mount', 'automount',
|
|
'swap', 'path', 'timer', 'snapshot', 'slice', 'scope', 'busname');
|
|
}
|
|
|
|
=head2 get_systemd_creatable_unit_types()
|
|
|
|
Returns systemd unit types that can be created as persistent unit files from
|
|
this module.
|
|
|
|
=cut
|
|
sub get_systemd_creatable_unit_types
|
|
{
|
|
return ('service', 'timer', 'socket', 'path', 'target');
|
|
}
|
|
|
|
=head2 get_systemd_list_unit_types()
|
|
|
|
Returns systemd unit types that should be listed by default.
|
|
|
|
=cut
|
|
sub get_systemd_list_unit_types
|
|
{
|
|
return ('service', 'timer', 'socket', 'path', 'target');
|
|
}
|
|
|
|
=head2 get_systemd_unit_type_from_name(name)
|
|
|
|
Returns the systemd unit type suffix from a full unit name, such as service or
|
|
timer, if it is a known unit type.
|
|
|
|
=cut
|
|
sub get_systemd_unit_type_from_name
|
|
{
|
|
my ($name) = @_;
|
|
return if (!defined($name));
|
|
my $units_piped = join('|', map { quotemeta } &get_systemd_unit_types());
|
|
return lc($1) if ($name =~ /\.($units_piped)$/i);
|
|
return;
|
|
}
|
|
|
|
=head2 systemd_index_url([unit-name], [user-scope], [user])
|
|
|
|
Returns the init module index URL with the correct systemd tab selected when
|
|
the unit type or user scope is known.
|
|
|
|
=cut
|
|
sub systemd_index_url
|
|
{
|
|
my ($name, $user_scope, $user) = @_;
|
|
my @args;
|
|
if ($user_scope) {
|
|
push(@args, "mode=user");
|
|
if ($user) {
|
|
push(@args, "scope=user");
|
|
push(@args, "unituser=".&urlize($user));
|
|
}
|
|
}
|
|
else {
|
|
my $type = &get_systemd_unit_type_from_name($name);
|
|
my %list_types = map { $_, 1 } &get_systemd_list_unit_types();
|
|
push(@args, "mode=".&urlize($type)) if ($type && $list_types{$type});
|
|
}
|
|
return "index.cgi".(@args ? "?".join("&", @args) : "");
|
|
}
|
|
|
|
=head2 get_systemd_unit_section(type)
|
|
|
|
Returns the type-specific section name for a systemd unit type.
|
|
|
|
=cut
|
|
sub get_systemd_unit_section
|
|
{
|
|
my ($type) = @_;
|
|
my %sections = ( 'service' => 'Service',
|
|
'timer' => 'Timer',
|
|
'socket' => 'Socket',
|
|
'path' => 'Path',
|
|
'target' => 'Target' );
|
|
return $sections{$type};
|
|
}
|
|
|
|
=head2 get_systemd_default_install_target(type, [user-scope])
|
|
|
|
Returns the default WantedBy target for a new systemd unit.
|
|
|
|
=cut
|
|
sub get_systemd_default_install_target
|
|
{
|
|
my ($type, $user_scope) = @_;
|
|
my %targets = ( 'service' => $user_scope ? 'default.target' : 'multi-user.target',
|
|
'timer' => 'timers.target',
|
|
'socket' => 'sockets.target',
|
|
'path' => 'paths.target',
|
|
'target' => $user_scope ? 'default.target' : 'multi-user.target' );
|
|
return $targets{$type};
|
|
}
|
|
|
|
=head2 is_systemd_service(name)
|
|
|
|
Returns 1 if some unit is managed by systemd.
|
|
|
|
=cut
|
|
sub is_systemd_service
|
|
{
|
|
my ($name) = @_;
|
|
my $units_piped = join('|', &get_systemd_unit_types());
|
|
foreach my $s (&list_systemd_services(1)) {
|
|
if (($s->{'name'} eq $name ||
|
|
$s->{'name'} =~
|
|
/^$name\.($units_piped)$/) && !$s->{'legacy'}) {
|
|
return 1;
|
|
}
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
=head2 get_systemd_root([name], [packaged])
|
|
|
|
Returns the base directory for systemd unit config files
|
|
|
|
=cut
|
|
sub get_systemd_root
|
|
{
|
|
my ($name, $packaged) = @_;
|
|
# Default systemd paths
|
|
my $systemd_local_conf = "/etc/systemd/system";
|
|
my $systemd_unit_dir1 = "/usr/lib/systemd/system";
|
|
my $systemd_unit_dir2 = "/lib/systemd/system";
|
|
if ($name) {
|
|
foreach my $p ($systemd_local_conf, $systemd_unit_dir1,
|
|
$systemd_unit_dir2) {
|
|
foreach my $t (&get_systemd_unit_types()) {
|
|
return $p if (-r "$p/$name.$t");
|
|
}
|
|
return $p if (-r "$p/$name");
|
|
}
|
|
}
|
|
# Always use /etc/systemd/system for locally created units
|
|
return $systemd_local_conf if (!$packaged && -d $systemd_local_conf);
|
|
|
|
# Debian prefers /lib/systemd/system
|
|
if ($gconfig{'os_type'} eq 'debian-linux' &&
|
|
-d $systemd_unit_dir2) {
|
|
return $systemd_unit_dir2;
|
|
}
|
|
# RHEL and other systems /usr/lib/systemd/system
|
|
if (-d $systemd_unit_dir1) {
|
|
return $systemd_unit_dir1;
|
|
}
|
|
# Fallback path for other systems
|
|
return $systemd_unit_dir2;
|
|
}
|
|
|
|
|
|
=head2 get_systemd_unit_pid([name])
|
|
|
|
Returns pid of running systemd unit
|
|
Returns 0 if unit stopped or missing
|
|
|
|
=cut
|
|
sub get_systemd_unit_pid
|
|
{
|
|
my ($unit) = @_;
|
|
my $pid =
|
|
&backquote_command("systemctl show --property MainPID @{[quotemeta($unit)]}");
|
|
$pid =~ s/MainPID=(\d+)/$1/;
|
|
$pid = int($pid);
|
|
return $pid;
|
|
}
|
|
|
|
=head2 restart_systemd()
|
|
|
|
Tell the systemd daemon to re-read its config
|
|
|
|
=cut
|
|
sub restart_systemd
|
|
{
|
|
if (&has_command("systemctl")) {
|
|
&system_logged("systemctl daemon-reload >/dev/null 2>&1");
|
|
}
|
|
else {
|
|
my @pids = &find_byname("systemd");
|
|
if (@pids) {
|
|
&kill_logged('HUP', @pids);
|
|
}
|
|
}
|
|
}
|
|
|
|
=head2 is_active_systemd(unit-name)
|
|
|
|
Check if a systemd unit is active.
|
|
|
|
=cut
|
|
sub is_active_systemd
|
|
{
|
|
my $unit = shift;
|
|
if ($init_mode eq "systemd") {
|
|
my $out = &backquote_logged(
|
|
"systemctl is-active ".quotemeta($unit)." 2>&1 </dev/null");
|
|
$out = &trim($out);
|
|
return wantarray ? ($?, $out) : $out eq "active" ? 1 : 0;
|
|
}
|
|
return wantarray ? (-1, undef) : 0;
|
|
}
|
|
|
|
=head2 cat_systemd(unit, [regex-filter])
|
|
|
|
List the contents of a given systemd unit file, alternatively, uses a regex
|
|
filter for specific options
|
|
|
|
=cut
|
|
sub cat_systemd
|
|
{
|
|
my ($unit, $filter) = @_;
|
|
my @config;
|
|
my $current_section;
|
|
my $current_file;
|
|
|
|
# Execute and parse the system command
|
|
&open_execute_command(*CAT, "systemctl cat ".quotemeta($unit), 1, 1);
|
|
while (<CAT>) {
|
|
s/\r|\n//g;
|
|
next if /^$/;
|
|
if (/^#\s+(\/.*)$/) {
|
|
# File name line, e.g., # /usr/lib/systemd/system/ssh.socket
|
|
$current_file = $1;
|
|
push @config, { file => $current_file, sections => {} };
|
|
}
|
|
elsif (/^\[(.+?)\]$/) {
|
|
# Section header, e.g., [Unit]
|
|
$current_section = $1;
|
|
$config[-1]{'sections'}{$current_section} ||= {};
|
|
}
|
|
elsif (/^([^=]+)=(.*)$/ && $current_section) {
|
|
# Key-value pair, e.g., ListenStream=0.0.0.0:22
|
|
my ($key, $value) = ($1, $2);
|
|
push @{ $config[-1]{'sections'}{$current_section}{$key} }, $value;
|
|
}
|
|
}
|
|
close(CAT);
|
|
|
|
# Filter specific options
|
|
if ($filter) {
|
|
my $regex = qr/$filter/;
|
|
$regex = eval "qr/$1/$2" if ($filter =~ m{^/(.+)/([igmsx]*)$});
|
|
foreach my $conf (@config) {
|
|
my $filtered_sections = {};
|
|
foreach my $section_name (keys %{$conf->{'sections'}}) {
|
|
my $section = $conf->{'sections'}{$section_name};
|
|
my %matching_params;
|
|
foreach my $param (keys %$section) {
|
|
$matching_params{$param} = $section->{$param}
|
|
if ($param =~ $regex);
|
|
}
|
|
$filtered_sections->{$section_name} =
|
|
\%matching_params if %matching_params;
|
|
}
|
|
$conf->{'sections'} = $filtered_sections;
|
|
}
|
|
}
|
|
return \@config;
|
|
}
|
|
|
|
=head2 edit_systemd(unit-name, &new_config, [override_filename], [override_dir])
|
|
|
|
Edit systemd unit file in override, preserving existing settings
|
|
|
|
Example:
|
|
|
|
edit_systemd('ssh.socket', {
|
|
'Socket' => {
|
|
'ListenStream' => [
|
|
'',
|
|
'0.0.0.0:2213',
|
|
'[::]:2213'
|
|
],
|
|
},
|
|
'Install' => {},
|
|
});
|
|
|
|
Note that option values must always be an array reference, even if there is only
|
|
one value; if undef is passed, the key will be removed from the section; if a
|
|
section set to empty hash, the section will be removed from the unit file.
|
|
|
|
=cut
|
|
sub edit_systemd
|
|
{
|
|
my ($unit, $new_config, $override_filename, $override_dir) = @_;
|
|
$override_dir ||= "/etc/systemd/system/$unit.d";
|
|
$override_filename ||= "override.conf";
|
|
my $override_file = "$override_dir/$override_filename";
|
|
|
|
# Create override directory if it doesn't exist
|
|
if (!-d($override_dir)) {
|
|
&make_dir($override_dir, 0755, 0) ||
|
|
&error("Failed to create directory '$override_dir': $!");
|
|
}
|
|
|
|
# Read the existing override.conf if it exists
|
|
my $existing_config = {};
|
|
if (-f($override_file)) {
|
|
my $content = &read_file_contents($override_file);
|
|
my $current_section;
|
|
foreach my $line (split(/\r?\n/, $content)) {
|
|
next if ($line =~ /^$/ || $line =~ /^#/);
|
|
if ($line =~ /^\[(.+?)\]$/) {
|
|
# Section header
|
|
$current_section = $1;
|
|
$existing_config->{$current_section} ||= {};
|
|
}
|
|
elsif ($line =~ /^([^=]+)=(.*)$/ && $current_section) {
|
|
# Key-value pair
|
|
my ($key, $value) = ($1, $2);
|
|
push(@{ $existing_config->{$current_section}{$key} },
|
|
$value);
|
|
}
|
|
}
|
|
}
|
|
|
|
# Merge new configuration into the existing configuration
|
|
foreach my $section (keys(%{$new_config})) {
|
|
my $has_values = 0; # Track if the section has content
|
|
foreach my $key (keys(%{ $new_config->{$section} })) {
|
|
my $values = $new_config->{$section}{$key};
|
|
if (defined($values) && @$values) {
|
|
# Preserve keys with values, including empty strings
|
|
$existing_config->{$section}{$key} = $values;
|
|
$has_values = 1;
|
|
}
|
|
else {
|
|
# Remove keys with undefined values
|
|
delete($existing_config->{$section}{$key});
|
|
}
|
|
}
|
|
# Remove the section if no content remains
|
|
delete($existing_config->{$section}) if (!$has_values);
|
|
}
|
|
|
|
# Prepare the new override.conf content
|
|
my $override_content = "";
|
|
foreach my $section (sort(keys(%{$existing_config}))) {
|
|
$override_content .= "[$section]\n";
|
|
foreach my $key (sort(keys(%{ $existing_config->{$section} }))) {
|
|
foreach my $value (@{ $existing_config->{$section}{$key} }) {
|
|
$override_content .= "$key=$value\n";
|
|
}
|
|
}
|
|
$override_content .= "\n"; # Add a blank line between sections
|
|
}
|
|
|
|
# Write the merged configuration back to override.conf
|
|
&lock_file($override_file);
|
|
&write_file_contents($override_file, $override_content);
|
|
&unlock_file($override_file);
|
|
|
|
# Reload systemd to apply the changes
|
|
&system_logged("systemctl daemon-reload") == 0 ||
|
|
&error("Failed to reload systemd daemon: $!");
|
|
}
|
|
|
|
=head2 reboot_system
|
|
|
|
Immediately reboots the system.
|
|
|
|
=cut
|
|
sub reboot_system
|
|
{
|
|
&system_logged("$config{'reboot_command'} >$null_file 2>$null_file");
|
|
}
|
|
|
|
=head2 shutdown_system
|
|
|
|
Immediately shuts down the system.
|
|
|
|
=cut
|
|
sub shutdown_system
|
|
{
|
|
&system_logged("$config{'shutdown_command'} >$null_file 2>$null_file");
|
|
}
|
|
|
|
=head2 get_action_args(filename)
|
|
|
|
Returns the args that this action script appears to support, like stop, start
|
|
and status.
|
|
|
|
=cut
|
|
sub get_action_args
|
|
{
|
|
my ($file) = @_;
|
|
my %hasarg;
|
|
open(FILE, "<".$file);
|
|
while(<FILE>) {
|
|
if (/^\s*(['"]?)([a-z]+)\1\)/i) {
|
|
$hasarg{$2}++;
|
|
}
|
|
}
|
|
close(FILE);
|
|
return \%hasarg;
|
|
}
|
|
|
|
=head2 action_running(filename)
|
|
|
|
Assuming some init.d action supports the status parameter, returns a 1 if
|
|
running, 0 if not, or -1 if unknown
|
|
|
|
=cut
|
|
sub action_running
|
|
{
|
|
my ($file) = @_;
|
|
&clean_language();
|
|
my ($out, $timedout) = &backquote_with_timeout("$file status", 2);
|
|
&reset_environment();
|
|
if ($timedout) {
|
|
return -1;
|
|
}
|
|
elsif ($out =~ /not\s+running/i ||
|
|
$out =~ /no\s+server\s+running/i) {
|
|
return 0;
|
|
}
|
|
elsif ($out =~ /running/i) {
|
|
return 1;
|
|
}
|
|
elsif ($out =~ /stopped/i) {
|
|
return 0;
|
|
}
|
|
return -1;
|
|
}
|
|
|
|
=head2 list_launchd_agents()
|
|
|
|
Returns an array of hash refs, each of which is a launchd daemon/agent
|
|
|
|
=cut
|
|
sub list_launchd_agents
|
|
{
|
|
my @rv;
|
|
|
|
# Get the initial list of actions
|
|
my $out = &backquote_command("launchctl list");
|
|
&error("Failed to list launchd agents : $out") if ($?);
|
|
foreach my $l (split(/\r?\n/, $out)) {
|
|
next if ($l =~ /^PID/); # Header line
|
|
my ($pid, $status, $label) = split(/\s+/, $l);
|
|
next if ($label =~ /^0x/); # Not really a launchd job
|
|
next if ($label =~ /\.peruser\./); # Skip user-owned actions
|
|
push(@rv, { 'name' => $label,
|
|
'status' => $pid eq "-" ? 0 : 1,
|
|
'pid' => $pid eq "-" ? undef : $pid, });
|
|
}
|
|
|
|
# Build map from plist files to agents
|
|
my @dirs = ("/Library/LaunchAgents",
|
|
"/Library/LaunchDaemons",
|
|
"/System/Library/LaunchAgents",
|
|
"/System/Library/LaunchDaemons");
|
|
my (%pmap, %runatload);
|
|
foreach my $dir (@dirs) {
|
|
foreach my $file (glob("$dir/*.plist")) {
|
|
my $plist = &read_file_contents($file);
|
|
if ($plist =~ /<key>Label<\/key>\s*<string>([^<]+)/i) {
|
|
$pmap{$1} = $file;
|
|
}
|
|
if ($plist =~ /<key>RunAtLoad<\/key>\s*<(true|false)\/>/i) {
|
|
$runatload{$file} = $1;
|
|
}
|
|
}
|
|
}
|
|
|
|
# Get details on each one
|
|
foreach my $a (@rv) {
|
|
my $out = &backquote_command("launchctl list ".quotemeta($a->{'name'}));
|
|
my %attrs;
|
|
foreach my $l (split(/\r?\n/, $out)) {
|
|
if ($l =~ /"(\S+)"\s*=\s*"([^"]*)";/ ||
|
|
$l =~ /"(\S+)"\s*=\s*(\S+);/) {
|
|
$attrs{lc($1)} = $2;
|
|
}
|
|
}
|
|
$a->{'start'} = $attrs{'program'};
|
|
$a->{'file'} = $pmap{$a->{'name'}};
|
|
$a->{'boot'} = $runatload{$a->{'file'}} eq 'true';
|
|
}
|
|
|
|
return @rv;
|
|
}
|
|
|
|
=head2 create_launchd_agent(name, start-script, boot-flag, [load-now])
|
|
|
|
Creates a new my launchd agent
|
|
|
|
=cut
|
|
sub create_launchd_agent
|
|
{
|
|
my ($name, $start, $boot, $load) = @_;
|
|
$load = 1 if (!defined($load));
|
|
my $file = "/Library/LaunchDaemons/".$name.".plist";
|
|
my $plist = "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n".
|
|
"<!DOCTYPE plist PUBLIC \"-//Apple//DTD PLIST 1.0//EN\" \"http://www.apple.com/DTDs/PropertyList-1.0.dtd\">\n".
|
|
"<plist version=\"1.0\">\n".
|
|
"<dict>\n".
|
|
"<key>Label</key>\n";
|
|
$plist .= "<string>$name</string>\n";
|
|
$plist .= "<key>ProgramArguments</key>\n";
|
|
$plist .= "<array>\n";
|
|
foreach my $a (&split_quoted_string($start)) {
|
|
$plist .= "<string>$a</string>\n";
|
|
}
|
|
$plist .= "</array>\n";
|
|
$plist .= "<key>RunAtLoad</key>\n";
|
|
$plist .= ($boot ? "<true/>\n" : "<false/>\n");
|
|
$plist .= "<key>KeepAlive</key>\n";
|
|
$plist .= "<false/>\n";
|
|
$plist .= "</dict>\n";
|
|
$plist .= "</plist>\n";
|
|
&open_lock_tempfile(PLIST, ">$file");
|
|
&print_tempfile(PLIST, $plist);
|
|
&close_tempfile(PLIST);
|
|
if ($load) {
|
|
my $out = &backquote_logged("launchctl load ".quotemeta($file)." 2>&1");
|
|
&error("Failed to load plist : $out") if ($?);
|
|
}
|
|
}
|
|
|
|
=head2 delete_launchd_agent(name)
|
|
|
|
Stop and remove the agent with some name
|
|
|
|
=cut
|
|
sub delete_launchd_agent
|
|
{
|
|
my ($name) = @_;
|
|
&system_logged("launchctl stop ".quotemeta($name)." 2>&1");
|
|
&system_logged("launchctl remove ".quotemeta($name)." 2>&1");
|
|
my ($a) = grep { $_->{'name'} eq $name } &list_launchd_agents();
|
|
if ($a && $a->{'file'} && -f $a->{'file'}) {
|
|
&system_logged("launchctl unload ".quotemeta($a->{'file'})." 2>&1");
|
|
&unlink_logged($a->{'file'});
|
|
}
|
|
}
|
|
|
|
=head2 stop_launchd_agent(name)
|
|
|
|
Kill the launchd daemon with some name
|
|
|
|
=cut
|
|
sub stop_launchd_agent
|
|
{
|
|
my ($name) = @_;
|
|
my $out = &backquote_logged(
|
|
"launchctl stop ".quotemeta($name)." 2>&1 </dev/null");
|
|
return (!$?, $out);
|
|
}
|
|
|
|
=head2 start_launchd_agent(name)
|
|
|
|
Startup the launchd daemon with some name
|
|
|
|
=cut
|
|
sub start_launchd_agent
|
|
{
|
|
my ($name) = @_;
|
|
my $out = &backquote_logged(
|
|
"launchctl start ".quotemeta($name)." 2>&1 </dev/null");
|
|
return (!$?, $out);
|
|
}
|
|
|
|
# launchd_name(name)
|
|
# If an action name isn't fully qualified, prepend com.webmin to it
|
|
sub launchd_name
|
|
{
|
|
my ($name) = @_;
|
|
return $name =~ /\./ ? $name : "com.webmin.".$name;
|
|
}
|
|
|
|
# config_pre_load(mod-info, [mod-order])
|
|
# Check if some config options are conditional
|
|
sub config_pre_load
|
|
{
|
|
my ($modconf_info, $modconf_order) = @_;
|
|
$modconf_info->{'desc'} =~ s/2-[^,]+,// if ($init_mode eq "systemd");
|
|
}
|
|
|
|
1;
|