mirror of
https://github.com/webmin/webmin.git
synced 2026-06-24 13:00:30 +01:00
1137 lines
42 KiB
Perl
Executable File
1137 lines
42 KiB
Perl
Executable File
#!/usr/local/bin/perl
|
|
# Show a form for creating, editing or viewing a systemd unit
|
|
|
|
use strict;
|
|
use warnings;
|
|
|
|
require './systemd-lib.pl'; ## no critic
|
|
|
|
our (%access, %config, %in, %text, $remote_user);
|
|
|
|
# Returns safe extra attributes for create-form placeholders.
|
|
sub placeholder_tags
|
|
{
|
|
my ($text, $tags) = @_;
|
|
my $rv = "placeholder=\"".quote_escape($text)."\"";
|
|
$rv .= " ".$tags if ($tags);
|
|
return $rv;
|
|
}
|
|
|
|
# Returns a create-form text box with a short example placeholder.
|
|
sub placeholder_textbox
|
|
{
|
|
my ($name, $value, $size, $placeholder) = @_;
|
|
return ui_textbox($name, $value, $size, undef, undef,
|
|
placeholder_tags($placeholder));
|
|
}
|
|
|
|
# Returns a create-form file box with a path-style placeholder.
|
|
sub placeholder_filebox
|
|
{
|
|
my ($name, $value, $size, $placeholder, $dironly) = @_;
|
|
return ui_filebox($name, $value, $size, undef, undef,
|
|
placeholder_tags($placeholder), $dironly);
|
|
}
|
|
|
|
# Returns a create-form text area with a command or directive example.
|
|
sub placeholder_textarea
|
|
{
|
|
my ($name, $value, $rows, $cols, $placeholder, $tags) = @_;
|
|
return ui_textarea($name, $value, $rows, $cols, undef, undef,
|
|
placeholder_tags($placeholder, $tags));
|
|
}
|
|
|
|
# Returns example home and runtime paths for user-scope placeholders.
|
|
sub user_scope_example_paths
|
|
{
|
|
my ($user) = @_;
|
|
my $uinfo = get_user_details($user);
|
|
my $home = $uinfo ? $uinfo->{'home'} : "/home/my-user";
|
|
my $runtime = $uinfo ? "/run/user/".$uinfo->{'uid'} : $home."/run";
|
|
return ($home, $runtime);
|
|
}
|
|
|
|
# Returns path-unit placeholders appropriate for system or user scope.
|
|
sub path_unit_placeholders
|
|
{
|
|
my ($user_scope, $user) = @_;
|
|
my %rv = (
|
|
'exists' => "/run/my-app.ready",
|
|
'existsglob' => "/var/spool/my-app/*.job",
|
|
'changed' => "/etc/my-app.conf",
|
|
'modified' => "/etc/my-app.d",
|
|
'directorynotempty' => "/var/spool/my-app",
|
|
);
|
|
if ($user_scope) {
|
|
my ($home, $runtime) = user_scope_example_paths($user);
|
|
%rv = (
|
|
'exists' => $runtime."/my-app.ready",
|
|
'existsglob' => $home."/spool/my-app/*.job",
|
|
'changed' => $home."/.config/my-app.conf",
|
|
'modified' => $home."/.config/my-app.d",
|
|
'directorynotempty' => $home."/spool/my-app",
|
|
);
|
|
}
|
|
return \%rv;
|
|
}
|
|
|
|
# Returns service-file placeholders appropriate for system or user scope.
|
|
sub service_unit_placeholders
|
|
{
|
|
my ($user_scope, $user) = @_;
|
|
my %rv = (
|
|
'pidfile' => "/run/my-app.pid",
|
|
'envfile' => "-/etc/default/my-app",
|
|
'workdir' => "/srv/my-app",
|
|
'readwritepaths' => "/var/lib/my-app",
|
|
'startpre' => "/usr/bin/install -d /run/my-app",
|
|
'stoppost' => "/usr/bin/rm -f /run/my-app.pid",
|
|
);
|
|
if ($user_scope) {
|
|
my ($home, $runtime) = user_scope_example_paths($user);
|
|
%rv = (
|
|
'pidfile' => $runtime."/my-app.pid",
|
|
'envfile' => "-".$home."/.config/my-app/env",
|
|
'workdir' => $home."/my-app",
|
|
'readwritepaths' => $home."/my-app",
|
|
'startpre' => "/usr/bin/install -d ".$runtime."/my-app",
|
|
'stoppost' => "/usr/bin/rm -f ".$runtime."/my-app.pid",
|
|
);
|
|
}
|
|
return \%rv;
|
|
}
|
|
|
|
# Returns socket placeholders appropriate for system or user scope.
|
|
sub socket_unit_placeholders
|
|
{
|
|
my ($user_scope, $user) = @_;
|
|
my %rv = ( 'listenfifo' => "/run/my-app.fifo" );
|
|
if ($user_scope) {
|
|
my (undef, $runtime) = user_scope_example_paths($user);
|
|
$rv{'listenfifo'} = $runtime."/my-app.fifo";
|
|
}
|
|
return \%rv;
|
|
}
|
|
|
|
ReadParse();
|
|
|
|
# Work out whether this page is creating or editing a user-scoped unit.
|
|
# User-scope units live in the selected Unix user's systemd manager.
|
|
my $unituser = clean_unit_value($in{'unituser'} || $in{'user'});
|
|
my $edit_user_scope = !$in{'new'} && $in{'scope'} eq 'user' ? 1 : 0;
|
|
my $create_default_user_scope = $in{'new'} && !defined($in{'scope'}) &&
|
|
$config{'default_create_scope'} eq 'user' ? 1 : 0;
|
|
my $create_user_scope = $in{'new'} &&
|
|
(($in{'scope'} || "") eq 'user' || $create_default_user_scope) ? 1 : 0;
|
|
my $edit_dropin = !$in{'new'} && $in{'dropin'} ? 1 : 0;
|
|
my $dropin_file = $edit_dropin ? clean_unit_value($in{'dropfile'}) : "";
|
|
my $dropin_info;
|
|
if ($in{'new'} && $create_user_scope && !$unituser) {
|
|
$unituser = systemd_acl_default_user(\%access) || "";
|
|
if (!$unituser) {
|
|
my $ruinfo = get_user_details($remote_user);
|
|
$unituser = $ruinfo->{'user'}
|
|
if ($ruinfo && $ruinfo->{'uid'} != 0);
|
|
}
|
|
}
|
|
if (!$in{'new'}) {
|
|
valid_unit_name($in{'name'}) ||
|
|
error($text{'systemd_ename'});
|
|
}
|
|
my ($u, $conf);
|
|
my (@units, @unittypes, @types, @killmodes, @restarts, @protects);
|
|
my (%creatable_types);
|
|
my $default_unittype = 'service';
|
|
my $unit_file_editable = 0;
|
|
my $can_save_unit = 0;
|
|
my $remote_uinfo = get_user_details($remote_user);
|
|
|
|
# New units start with an empty record. Existing units are looked up from the
|
|
# selected system or user scope so edits cannot cross scopes accidentally.
|
|
if ($in{'new'}) {
|
|
systemd_can_create(\%access, $create_user_scope,
|
|
$create_user_scope ? $unituser : undef) ||
|
|
systemd_acl_error($create_user_scope ?
|
|
'pcreate_user' : 'pcreate');
|
|
# The create form renders structured fields instead of raw unit contents.
|
|
ui_print_header(undef, $text{'systemd_title1'}, "");
|
|
$u = { };
|
|
}
|
|
else {
|
|
# Editing keeps the unit in its current system or user manager.
|
|
if ($edit_user_scope) {
|
|
# The owner must be a real Unix user before we inspect their units.
|
|
get_user_details($unituser) ||
|
|
error($text{'systemd_euser'});
|
|
systemd_can_view_scope(\%access, 1, $unituser) ||
|
|
systemd_acl_error('pview_user');
|
|
@units = list_user_units($unituser);
|
|
}
|
|
else {
|
|
# System-scope edits use the system unit list.
|
|
systemd_can_view_scope(\%access, 0) ||
|
|
systemd_acl_error('pview');
|
|
@units = list_units();
|
|
}
|
|
|
|
# Reject stale edit links after units have been deleted or renamed.
|
|
($u) = grep { $_->{'name'} eq $in{'name'} } @units;
|
|
$u || error($text{'systemd_egone'});
|
|
if ($edit_dropin && $dropin_file) {
|
|
$dropin_info = $edit_user_scope ?
|
|
user_dropin_config_file_info($unituser, $dropin_file) :
|
|
system_dropin_config_file_info($dropin_file);
|
|
$dropin_info && $dropin_info->{'unit'} eq $in{'name'} ||
|
|
error($text{'systemd_edropinfile'});
|
|
$dropin_file = $dropin_info->{'file'};
|
|
}
|
|
$unit_file_editable = unit_file_editable($u);
|
|
$can_save_unit = $edit_dropin ?
|
|
systemd_can_dropin(\%access, $edit_user_scope, $unituser) :
|
|
systemd_can_edit(\%access, $edit_user_scope, $unituser);
|
|
|
|
# Runtime-managed units are inspect-only, so title them as views.
|
|
my $title_key = $unit_file_editable && $can_save_unit ?
|
|
($edit_user_scope ? 'systemd_title2_user' : 'systemd_title2') :
|
|
($edit_user_scope ? 'systemd_title2_view_user' :
|
|
'systemd_title2_view');
|
|
ui_print_header(undef, $text{$title_key}, "");
|
|
}
|
|
|
|
# The save script uses hidden scope fields to pick the correct control plane
|
|
# for later actions, including status/log redirects.
|
|
print ui_form_start("save_unit.cgi", "post");
|
|
print ui_hidden("new", $in{'new'});
|
|
print ui_hidden("scope", "user") if ($edit_user_scope);
|
|
print ui_hidden("unituser", $unituser) if ($edit_user_scope);
|
|
print ui_hidden("name", $in{'name'}) if (!$in{'new'});
|
|
print ui_hidden("dropin", 1) if ($edit_dropin);
|
|
print ui_hidden("dropfile", $dropin_file) if ($edit_dropin && $dropin_file);
|
|
if ($in{'new'}) {
|
|
# The first table contains the fields that almost every new unit needs.
|
|
print ui_table_start($text{'systemd_header'}, undef, 2);
|
|
|
|
# Unit type and name. The suffix is displayed separately, but the save
|
|
# script appends or validates it before writing the unit file.
|
|
my @creatable_unit_types = get_creatable_unit_types($create_user_scope);
|
|
@unittypes = map { [ $_, $text{'systemd_type_'.$_} || $_ ] }
|
|
@creatable_unit_types;
|
|
%creatable_types = map { $_, 1 } @creatable_unit_types;
|
|
$default_unittype = $creatable_types{$in{'unittype'}} ?
|
|
$in{'unittype'} : "service";
|
|
my $type_help = $create_user_scope ?
|
|
"systemd_type_user" : "systemd_type";
|
|
print ui_table_row(hlink($text{'systemd_type'}, $type_help),
|
|
ui_select("unittype", $default_unittype, \@unittypes,
|
|
1, 0, 0, 0));
|
|
print ui_table_hr();
|
|
print ui_table_row(hlink($text{'systemd_name'}, "systemd_name"),
|
|
placeholder_textbox("name", undef, 30, "my-app").
|
|
ui_tag('tt', ".$default_unittype",
|
|
{ 'id' => 'systemd_name_suffix' }));
|
|
|
|
# Every new unit needs a Description= line.
|
|
print ui_table_row(hlink($text{'systemd_desc'}, "systemd_desc"),
|
|
placeholder_textbox("desc", undef, 60,
|
|
"My app service"));
|
|
|
|
# Existing mount units can be paired with a new automount, so the user
|
|
# does not need to derive the automount name by hand.
|
|
my @mount_units;
|
|
if ($create_user_scope && get_user_details($unituser)) {
|
|
@mount_units = grep { $_->{'name'} =~ /\.mount$/ }
|
|
list_user_units($unituser);
|
|
}
|
|
elsif (!$create_user_scope) {
|
|
@mount_units = grep { $_->{'name'} =~ /\.mount$/ }
|
|
list_units();
|
|
}
|
|
my @automount_mounts = ( [ '', $text{'systemd_automountmount_none'} ] );
|
|
foreach my $mu (sort { $a->{'name'} cmp $b->{'name'} } @mount_units) {
|
|
my $where = mount_unit_where(
|
|
$mu, $create_user_scope ? $unituser : undef);
|
|
my $label = $mu->{'name'}.
|
|
($where ? " (".$where.")" : "");
|
|
push(@automount_mounts, [ $mu->{'name'}, html_escape($label) ]);
|
|
}
|
|
|
|
# Service units use command fields rather than raw [Service] body text.
|
|
print ui_table_row(hlink($text{'systemd_start'}, "systemd_start"),
|
|
placeholder_textarea("atstart", undef, 5, 80,
|
|
"/usr/bin/my-app --foreground"),
|
|
1, undef, [ "data-systemd-service='1'" ]);
|
|
|
|
# The stop command is optional; the save page can generate a default.
|
|
print ui_table_row(hlink($text{'systemd_stop'}, "systemd_stop"),
|
|
placeholder_textarea("atstop", undef, 5, 80,
|
|
"/bin/kill -TERM \$MAINPID"),
|
|
1, undef, [ "data-systemd-service='1'" ]);
|
|
|
|
# Timer units can be created from common activation fields. More unusual
|
|
# timer directives remain available in the advanced body editor below.
|
|
my @timer_row = ( "data-systemd-timer='1' style='display:none'" );
|
|
print ui_table_row(hlink($text{'systemd_timeroncalendar'},
|
|
"systemd_timeroncalendar"),
|
|
placeholder_textbox("timer_oncalendar", undef, 40,
|
|
"daily"),
|
|
1, undef, \@timer_row);
|
|
print ui_table_row(hlink($text{'systemd_timeronbootsec'},
|
|
"systemd_timeronbootsec"),
|
|
placeholder_textbox("timer_onbootsec", undef, 10,
|
|
"5min"),
|
|
1, undef, \@timer_row);
|
|
print ui_table_row(hlink($text{'systemd_timeronunitactivesec'},
|
|
"systemd_timeronunitactivesec"),
|
|
placeholder_textbox("timer_onunitactivesec", undef,
|
|
10, "1h"),
|
|
1, undef, \@timer_row);
|
|
print ui_table_row(hlink($text{'systemd_timerpersistent'},
|
|
"systemd_timerpersistent"),
|
|
ui_yesno_radio("timer_persistent", 0),
|
|
1, undef, \@timer_row);
|
|
print ui_table_row(hlink($text{'systemd_timerrandomizeddelaysec'},
|
|
"systemd_timerrandomizeddelaysec"),
|
|
placeholder_textbox("timer_randomizeddelaysec", undef,
|
|
10, "10min"),
|
|
1, undef, \@timer_row);
|
|
print ui_table_row(hlink($text{'systemd_timeraccuracysec'},
|
|
"systemd_timeraccuracysec"),
|
|
placeholder_textbox("timer_accuracysec", undef, 10,
|
|
"1min"),
|
|
1, undef, \@timer_row);
|
|
print ui_table_row(hlink($text{'systemd_timerunit'},
|
|
"systemd_timerunit"),
|
|
placeholder_textbox("timer_unit", undef, 40,
|
|
"my-job.service"),
|
|
1, undef, \@timer_row);
|
|
|
|
# Socket units expose the usual listeners and ownership controls.
|
|
my @socket_row = ( "data-systemd-socket='1' style='display:none'" );
|
|
my $socket_placeholders =
|
|
socket_unit_placeholders($create_user_scope, $unituser);
|
|
print ui_table_row(hlink($text{'systemd_socketlistenstream'},
|
|
"systemd_socketlistenstream"),
|
|
placeholder_textbox("socket_listenstream", undef, 40,
|
|
"127.0.0.1:8080"),
|
|
1, undef, \@socket_row);
|
|
print ui_table_row(hlink($text{'systemd_socketlistendatagram'},
|
|
"systemd_socketlistendatagram"),
|
|
placeholder_textbox("socket_listendatagram", undef,
|
|
40, "10514"),
|
|
1, undef, \@socket_row);
|
|
print ui_table_row(hlink($text{'systemd_socketlistenfifo'},
|
|
"systemd_socketlistenfifo"),
|
|
placeholder_filebox("socket_listenfifo", undef, 50,
|
|
$socket_placeholders->{'listenfifo'}, 1),
|
|
1, undef, \@socket_row);
|
|
print ui_table_row(hlink($text{'systemd_socketaccept'},
|
|
"systemd_socketaccept"),
|
|
ui_yesno_radio("socket_accept", 0),
|
|
1, undef, \@socket_row);
|
|
print ui_table_row(hlink($text{'systemd_socketuser'},
|
|
"systemd_socketuser"),
|
|
placeholder_textbox("socket_user", undef, 20,
|
|
"appuser")." ".
|
|
user_chooser_button("socket_user"),
|
|
1, undef, [ "id='systemd_socket_user_row' ".
|
|
"data-systemd-socket='1' style='display:none'" ]);
|
|
print ui_table_row(hlink($text{'systemd_socketgroup'},
|
|
"systemd_socketgroup"),
|
|
placeholder_textbox("socket_group", undef, 20,
|
|
"appgroup")." ".
|
|
group_chooser_button("socket_group"),
|
|
1, undef, [ "id='systemd_socket_group_row' ".
|
|
"data-systemd-socket='1' style='display:none'" ]);
|
|
print ui_table_row(hlink($text{'systemd_socketmode'},
|
|
"systemd_socketmode"),
|
|
placeholder_textbox("socket_mode", undef, 10, "0660"),
|
|
1, undef, \@socket_row);
|
|
print ui_table_row(hlink($text{'systemd_socketservice'},
|
|
"systemd_socketservice"),
|
|
placeholder_textbox("socket_service", undef, 40,
|
|
"my-app.service"),
|
|
1, undef, \@socket_row);
|
|
|
|
# Path units watch files or directories and activate another unit.
|
|
my @path_row = ( "data-systemd-path='1' style='display:none'" );
|
|
my $path_placeholders =
|
|
path_unit_placeholders($create_user_scope, $unituser);
|
|
print ui_table_row(hlink($text{'systemd_pathexists'},
|
|
"systemd_pathexists"),
|
|
placeholder_filebox("path_exists", undef, 50,
|
|
$path_placeholders->{'exists'}, 1),
|
|
1, undef, \@path_row);
|
|
print ui_table_row(hlink($text{'systemd_pathexistsglob'},
|
|
"systemd_pathexistsglob"),
|
|
placeholder_textbox("path_existsglob", undef, 50,
|
|
$path_placeholders->{'existsglob'}),
|
|
1, undef, \@path_row);
|
|
print ui_table_row(hlink($text{'systemd_pathchanged'},
|
|
"systemd_pathchanged"),
|
|
placeholder_filebox("path_changed", undef, 50,
|
|
$path_placeholders->{'changed'}, 1),
|
|
1, undef, \@path_row);
|
|
print ui_table_row(hlink($text{'systemd_pathmodified'},
|
|
"systemd_pathmodified"),
|
|
placeholder_filebox("path_modified", undef, 50,
|
|
$path_placeholders->{'modified'}, 1),
|
|
1, undef, \@path_row);
|
|
print ui_table_row(hlink($text{'systemd_pathdirectorynotempty'},
|
|
"systemd_pathdirectorynotempty"),
|
|
placeholder_filebox("path_directorynotempty", undef,
|
|
50,
|
|
$path_placeholders->{'directorynotempty'}, 1),
|
|
1, undef, \@path_row);
|
|
print ui_table_row(hlink($text{'systemd_pathmakedirectory'},
|
|
"systemd_pathmakedirectory"),
|
|
ui_yesno_radio("path_makedirectory", 0),
|
|
1, undef, \@path_row);
|
|
print ui_table_row(hlink($text{'systemd_pathunit'},
|
|
"systemd_pathunit"),
|
|
placeholder_textbox("path_unit", undef, 40,
|
|
"my-app.service"),
|
|
1, undef, \@path_row);
|
|
|
|
# Mount units have a small, stable set of required fields. The unit name
|
|
# can be derived from Where= on save.
|
|
my @mount_row = ( "data-systemd-mount='1' style='display:none'" );
|
|
print ui_table_row(hlink($text{'systemd_mountwhat'}, "systemd_mountwhat"),
|
|
placeholder_textbox("mount_what", undef, 60,
|
|
"/dev/disk/by-uuid/abcd-1234"),
|
|
1, undef, \@mount_row);
|
|
print ui_table_row(hlink($text{'systemd_mountwhere'}, "systemd_mountwhere"),
|
|
placeholder_filebox("mount_where", undef, 50,
|
|
"/mnt/data", 1),
|
|
1, undef, \@mount_row);
|
|
print ui_table_row(hlink($text{'systemd_mounttype'}, "systemd_mounttype"),
|
|
placeholder_textbox("mount_type", undef, 20, "ext4"),
|
|
1, undef, \@mount_row);
|
|
print ui_table_row(hlink($text{'systemd_mountoptions'}, "systemd_mountoptions"),
|
|
placeholder_textbox("mount_options", undef, 60,
|
|
"defaults,noatime"),
|
|
1, undef, \@mount_row);
|
|
|
|
# Automount units activate a matching mount unit by path. Selecting an
|
|
# existing mount lets save_unit.cgi derive the automount name safely.
|
|
my @automount_row = (
|
|
"data-systemd-automount='1' style='display:none'" );
|
|
print ui_table_row(hlink($text{'systemd_automountmount'},
|
|
"systemd_automountmount"),
|
|
ui_select("automount_mount", undef,
|
|
\@automount_mounts),
|
|
1, undef, \@automount_row);
|
|
print ui_table_row(hlink($text{'systemd_automountwhere'},
|
|
"systemd_automountwhere"),
|
|
placeholder_filebox("automount_where", undef, 50,
|
|
"/mnt/data", 1),
|
|
1, undef, \@automount_row);
|
|
print ui_table_row(hlink($text{'systemd_automountidle'},
|
|
"systemd_automountidle"),
|
|
placeholder_textbox("automount_idle", undef, 10,
|
|
"5min"),
|
|
1, undef, \@automount_row);
|
|
print ui_table_row(hlink($text{'systemd_automountmode'},
|
|
"systemd_automountmode"),
|
|
placeholder_textbox("automount_mode", undef, 10,
|
|
"0755"),
|
|
1, undef, \@automount_row);
|
|
|
|
# Swap and slice units have a few common directives worth exposing.
|
|
my @swap_row = ( "data-systemd-swap='1' style='display:none'" );
|
|
print ui_table_row(hlink($text{'systemd_swapwhat'}, "systemd_swapwhat"),
|
|
placeholder_filebox("swap_what", undef, 50,
|
|
"/swapfile", 1),
|
|
1, undef, \@swap_row);
|
|
print ui_table_row(hlink($text{'systemd_swappriority'},
|
|
"systemd_swappriority"),
|
|
placeholder_textbox("swap_priority", undef, 10, "10"),
|
|
1, undef, \@swap_row);
|
|
print ui_table_row(hlink($text{'systemd_swapoptions'},
|
|
"systemd_swapoptions"),
|
|
placeholder_textbox("swap_options", undef, 60,
|
|
"discard"),
|
|
1, undef, \@swap_row);
|
|
print ui_table_row(hlink($text{'systemd_swaptimeoutsec'},
|
|
"systemd_swaptimeoutsec"),
|
|
placeholder_textbox("swap_timeoutsec", undef, 10,
|
|
"30s"),
|
|
1, undef, \@swap_row);
|
|
my @slice_row = ( "data-systemd-slice='1' style='display:none'" );
|
|
print ui_table_row(hlink($text{'systemd_slicecpuweight'},
|
|
"systemd_slicecpuweight"),
|
|
placeholder_textbox("slice_cpuweight", undef, 10,
|
|
"200"),
|
|
1, undef, \@slice_row);
|
|
print ui_table_row(hlink($text{'systemd_slicememorymax'},
|
|
"systemd_slicememorymax"),
|
|
placeholder_textbox("slice_memorymax", undef, 10,
|
|
"512M"),
|
|
1, undef, \@slice_row);
|
|
print ui_table_row(hlink($text{'systemd_slicetasksmax'},
|
|
"systemd_slicetasksmax"),
|
|
placeholder_textbox("slice_tasksmax", undef, 10,
|
|
"500"),
|
|
1, undef, \@slice_row);
|
|
print ui_table_row(hlink($text{'systemd_sliceioweight'},
|
|
"systemd_sliceioweight"),
|
|
placeholder_textbox("slice_ioweight", undef, 10,
|
|
"200"),
|
|
1, undef, \@slice_row);
|
|
|
|
# Startup state is applied after the unit is created.
|
|
if (systemd_can_boot(\%access, $create_user_scope,
|
|
$create_user_scope ? $unituser : undef)) {
|
|
print ui_table_row(hlink($text{'systemd_boot'}, "systemd_boot"),
|
|
ui_yesno_radio("boot", 1));
|
|
}
|
|
|
|
# Pick a safe default owner for new user units when possible.
|
|
my $default_unituser = $unituser;
|
|
my $acl_default_unituser;
|
|
if (!$default_unituser) {
|
|
$acl_default_unituser =
|
|
systemd_acl_default_user(\%access) || "";
|
|
$default_unituser = $acl_default_unituser;
|
|
if (!$default_unituser) {
|
|
$default_unituser = $remote_uinfo->{'user'}
|
|
if ($remote_uinfo &&
|
|
$remote_uinfo->{'uid'} != 0);
|
|
}
|
|
}
|
|
my $force_user_scope_create = $create_user_scope &&
|
|
!systemd_can_create(\%access, 0) &&
|
|
(($remote_uinfo && $remote_uinfo->{'uid'} != 0) ||
|
|
$acl_default_unituser) ? 1 : 0;
|
|
my $force_user_scope_owner = $force_user_scope_create &&
|
|
$default_unituser ? 1 : 0;
|
|
# User units live in the selected user's home and run under that user's
|
|
# systemd manager, so the service-level User=/Group= rows are hidden by JS.
|
|
if ($force_user_scope_create) {
|
|
print ui_hidden("userservice", 1);
|
|
}
|
|
else {
|
|
print ui_table_row(hlink($text{'systemd_userservice'},
|
|
"systemd_userservice"),
|
|
ui_radio("userservice",
|
|
$create_user_scope ? 1 : 0,
|
|
[ [ 1, $text{'yes'} ],
|
|
[ 0, $text{'no'} ] ]),
|
|
1, undef,
|
|
[ "id='systemd_userservice_row'" ]);
|
|
print ui_table_hr();
|
|
}
|
|
if ($force_user_scope_owner) {
|
|
print ui_hidden("unituser", $default_unituser);
|
|
}
|
|
else {
|
|
print ui_table_row(hlink($text{'systemd_unituser'},
|
|
"systemd_unituser"),
|
|
placeholder_textbox("unituser",
|
|
$default_unituser, 20,
|
|
"appuser")." ".
|
|
user_chooser_button("unituser"),
|
|
1, undef,
|
|
[ "id='systemd_unituser_row'".
|
|
($create_user_scope ? "" :
|
|
" style='display:none'") ]);
|
|
}
|
|
if (systemd_acl_bool(\%access, 'linger')) {
|
|
my $linger_text = $create_user_scope ?
|
|
$text{'systemd_linger_user'} : $text{'systemd_linger'};
|
|
my $linger_help = $create_user_scope ?
|
|
"systemd_linger_user" : "systemd_linger";
|
|
print ui_table_row(hlink($linger_text, $linger_help),
|
|
ui_yesno_radio("linger",
|
|
$config{'default_linger'} ? 1 : 0),
|
|
1, undef, [ "id='systemd_linger_row'".
|
|
($create_user_scope ? "" : " style='display:none'") ]);
|
|
}
|
|
|
|
print ui_table_end();
|
|
|
|
# Less common create-time settings are collapsed by default.
|
|
print ui_hidden_table_start($text{'systemd_advanced'}, undef, 2,
|
|
"advanced", 0);
|
|
|
|
# Unit relationships are shared by all creatable unit types and are written
|
|
# into the [Unit] section.
|
|
print ui_table_row(hlink($text{'systemd_before'}, "systemd_before"),
|
|
placeholder_textbox("before", undef, 60,
|
|
"network.target"));
|
|
print ui_table_row(hlink($text{'systemd_after'}, "systemd_after"),
|
|
placeholder_textbox("after", undef, 60,
|
|
"network-online.target"));
|
|
print ui_table_row(hlink($text{'systemd_wants'}, "systemd_wants"),
|
|
placeholder_textbox("wants", undef, 60,
|
|
"network-online.target"));
|
|
print ui_table_row(hlink($text{'systemd_requires'}, "systemd_requires"),
|
|
placeholder_textbox("requires", undef, 60,
|
|
"postgresql.service"));
|
|
print ui_table_row(hlink($text{'systemd_conflicts'}, "systemd_conflicts"),
|
|
placeholder_textbox("conflicts", undef, 60,
|
|
"old-app.service"));
|
|
print ui_table_row(hlink($text{'systemd_onfailure'}, "systemd_onfailure"),
|
|
placeholder_textbox("onfailure", undef, 60,
|
|
"notify@%n.service"));
|
|
print ui_table_row(hlink($text{'systemd_onsuccess'}, "systemd_onsuccess"),
|
|
placeholder_textbox("onsuccess", undef, 60,
|
|
"report.service"));
|
|
|
|
# Service options become irrelevant for all non-service unit types; each row
|
|
# is marked so the JS type switch can hide it.
|
|
my @service_row = ( "data-systemd-service='1'" );
|
|
my $service_placeholders =
|
|
service_unit_placeholders($create_user_scope, $unituser);
|
|
@types = ( [ '', $text{'default'} ], "simple", "exec", "forking",
|
|
"oneshot", "dbus", "notify", "idle" );
|
|
print ui_table_row(hlink($text{'systemd_servicetype'}, "systemd_servicetype"),
|
|
ui_select("type", undef, \@types),
|
|
1, undef, \@service_row);
|
|
print ui_table_row(hlink($text{'systemd_remain'}, "systemd_remain"),
|
|
ui_yesno_radio("remain", 0),
|
|
1, undef, \@service_row);
|
|
print ui_table_row(hlink($text{'systemd_pidfile'}, "systemd_pidfile"),
|
|
placeholder_filebox("pidfile", undef, 50,
|
|
$service_placeholders->{'pidfile'}),
|
|
1, undef, \@service_row);
|
|
print ui_table_row(hlink($text{'systemd_env'}, "systemd_env"),
|
|
placeholder_textbox("env", undef, 60,
|
|
"NODE_ENV=production PORT=8080"),
|
|
1, undef, \@service_row);
|
|
print ui_table_row(hlink($text{'systemd_envfile'}, "systemd_envfile"),
|
|
placeholder_filebox("envfile", undef, 50,
|
|
$service_placeholders->{'envfile'}),
|
|
1, undef, \@service_row);
|
|
print ui_table_row(hlink($text{'systemd_user'}, "systemd_user"),
|
|
placeholder_textbox("user", undef, 20, "appuser")." ".
|
|
user_chooser_button("user"),
|
|
1, undef, [ "id='systemd_runas_user_row' ".
|
|
"data-systemd-service='1'".
|
|
($create_user_scope ? " style='display:none'" : "") ]);
|
|
print ui_table_row(hlink($text{'systemd_group'}, "systemd_group"),
|
|
placeholder_textbox("group", undef, 20, "appgroup")." ".
|
|
group_chooser_button("group"),
|
|
1, undef, [ "id='systemd_runas_group_row' ".
|
|
"data-systemd-service='1'".
|
|
($create_user_scope ? " style='display:none'" : "") ]);
|
|
@killmodes = ( [ '', $text{'default'} ], "control-group",
|
|
"process", "mixed", "none" );
|
|
print ui_table_row(hlink($text{'systemd_killmode'}, "systemd_killmode"),
|
|
ui_select("killmode", undef, \@killmodes),
|
|
1, undef, \@service_row);
|
|
print ui_table_row(hlink($text{'systemd_workdir'}, "systemd_workdir"),
|
|
placeholder_filebox("workdir", undef, 50,
|
|
$service_placeholders->{'workdir'}, 1),
|
|
1, undef, \@service_row);
|
|
@restarts = ( [ '', $text{'default'} ], "no", "on-success",
|
|
"on-failure", "on-abnormal", "on-watchdog",
|
|
"on-abort", "always" );
|
|
print ui_table_row(hlink($text{'systemd_restart'}, "systemd_restart"),
|
|
ui_select("restart_policy", undef, \@restarts),
|
|
1, undef, \@service_row);
|
|
print ui_table_row(hlink($text{'systemd_restartsec'}, "systemd_restartsec"),
|
|
placeholder_textbox("restartsec", undef, 10, "5s"),
|
|
1, undef, \@service_row);
|
|
print ui_table_row(hlink($text{'systemd_watchdogsec'}, "systemd_watchdogsec"),
|
|
placeholder_textbox("watchdogsec", undef, 10, "30s"),
|
|
1, undef, \@service_row);
|
|
print ui_table_row(hlink($text{'systemd_timeout'}, "systemd_timeout"),
|
|
placeholder_textbox("timeoutstartsec", undef, 10,
|
|
"30s"),
|
|
1, undef, \@service_row);
|
|
print ui_table_row(hlink($text{'systemd_timeoutstop'}, "systemd_timeoutstop"),
|
|
placeholder_textbox("timeoutstopsec", undef, 10,
|
|
"30s"),
|
|
1, undef, \@service_row);
|
|
print ui_table_row(hlink($text{'systemd_limitnofile'}, "systemd_limitnofile"),
|
|
placeholder_textbox("limitnofile", undef, 10,
|
|
"65535"),
|
|
1, undef, \@service_row);
|
|
print ui_table_row(hlink($text{'systemd_logstd'}, "systemd_logstd"),
|
|
placeholder_textbox("logstd", undef, 50,
|
|
"journal"),
|
|
1, undef, \@service_row);
|
|
print ui_table_row(hlink($text{'systemd_logerr'}, "systemd_logerr"),
|
|
placeholder_textbox("logerr", undef, 50,
|
|
"journal"),
|
|
1, undef, \@service_row);
|
|
print ui_table_row(hlink($text{'systemd_syslogid'}, "systemd_syslogid"),
|
|
placeholder_textbox("syslogid", undef, 30, "my-app"),
|
|
1, undef, \@service_row);
|
|
print ui_table_row(hlink($text{'systemd_nonewprivs'}, "systemd_nonewprivs"),
|
|
ui_yesno_radio("nonewprivs", 0),
|
|
1, undef, \@service_row);
|
|
print ui_table_row(hlink($text{'systemd_privatetmp'}, "systemd_privatetmp"),
|
|
ui_yesno_radio("privatetmp", 0),
|
|
1, undef, \@service_row);
|
|
@protects = ( [ '', $text{'default'} ], "true", "full", "strict" );
|
|
print ui_table_row(hlink($text{'systemd_protectsystem'}, "systemd_protectsystem"),
|
|
ui_select("protectsystem", undef, \@protects),
|
|
1, undef, \@service_row);
|
|
print ui_table_row(hlink($text{'systemd_readwritepaths'}, "systemd_readwritepaths"),
|
|
placeholder_textbox("readwritepaths", undef, 60,
|
|
$service_placeholders->{'readwritepaths'}),
|
|
1, undef, \@service_row);
|
|
|
|
# Install options stay visible for all types. JS changes the default target
|
|
# when switching between system/user units or between unit types.
|
|
my $default_wantedby =
|
|
get_default_install_target($default_unittype,
|
|
$create_user_scope);
|
|
print ui_table_row(hlink($text{'systemd_wantedby'}, "systemd_wantedby"),
|
|
placeholder_textbox("wantedby", $default_wantedby,
|
|
60, "multi-user.target"));
|
|
|
|
# Extra non-service directives supplement the guided fields above. Only
|
|
# directive lines belong here; the renderer adds the correct section header.
|
|
print ui_table_row(hlink($text{'systemd_unitconf'}, "systemd_unitconf"),
|
|
placeholder_textarea("unitconf", undef, 8, 80,
|
|
"RuntimeMaxSec=1h", "spellcheck='false'"),
|
|
1, undef, [ "data-systemd-extra='1' ".
|
|
"style='display:none'" ]);
|
|
|
|
# Extra command hooks are service-only and are kept near the end because
|
|
# they are less commonly needed than the scalar service settings above.
|
|
print ui_table_row(hlink($text{'systemd_startpre'}, "systemd_startpre"),
|
|
placeholder_textarea("startpre", undef, 3, 80,
|
|
$service_placeholders->{'startpre'}),
|
|
1, undef, \@service_row);
|
|
print ui_table_row(hlink($text{'systemd_startpost'}, "systemd_startpost"),
|
|
placeholder_textarea("startpost", undef, 3, 80,
|
|
"/usr/bin/logger my-app started"),
|
|
1, undef, \@service_row);
|
|
print ui_table_row(hlink($text{'systemd_stoppost'}, "systemd_stoppost"),
|
|
placeholder_textarea("stoppost", undef, 3, 80,
|
|
$service_placeholders->{'stoppost'}),
|
|
1, undef, \@service_row);
|
|
print ui_table_row(hlink($text{'systemd_reload'}, "systemd_reload"),
|
|
placeholder_textarea("reload", undef, 3, 80,
|
|
"/bin/kill -HUP \$MAINPID"),
|
|
1, undef, \@service_row);
|
|
|
|
print ui_hidden_table_end("advanced");
|
|
my $systemd_js = <<'EOF';
|
|
(function() {
|
|
'use strict';
|
|
|
|
// Unit suffixes shown next to the editable base name.
|
|
const systemdSuffixes = {
|
|
service: '.service',
|
|
timer: '.timer',
|
|
socket: '.socket',
|
|
path: '.path',
|
|
target: '.target',
|
|
mount: '.mount',
|
|
automount: '.automount',
|
|
swap: '.swap',
|
|
slice: '.slice'
|
|
};
|
|
// Type-aware examples keep the create form from sounding service-only.
|
|
const systemdNamePlaceholders = {
|
|
service: 'my-app',
|
|
timer: 'nightly-backup',
|
|
socket: 'my-app',
|
|
path: 'config-watch',
|
|
target: 'app-stack',
|
|
mount: 'mnt-data',
|
|
automount: 'mnt-data',
|
|
swap: 'swapfile',
|
|
slice: 'app-workload'
|
|
};
|
|
const systemdDescPlaceholders = {
|
|
service: 'My app service',
|
|
timer: 'Nightly backup timer',
|
|
socket: 'My app socket',
|
|
path: 'Watch app config',
|
|
target: 'App stack target',
|
|
mount: 'Mount /mnt/data',
|
|
automount: 'Automount /mnt/data',
|
|
swap: 'Swap file',
|
|
slice: 'App resource slice'
|
|
};
|
|
const systemdExtraPlaceholders = {
|
|
timer: 'OnUnitInactiveSec=2min\nWakeSystem=no\nRemainAfterElapse=yes',
|
|
socket: 'Backlog=32\nKeepAlive=yes\nNoDelay=yes\nFileDescriptorName=my-app',
|
|
path: 'TriggerLimitIntervalSec=30s\nTriggerLimitBurst=10',
|
|
mount: 'TimeoutSec=30s\nLazyUnmount=yes',
|
|
automount: 'DirectoryMode=0755',
|
|
swap: 'TimeoutSec=30s',
|
|
slice: 'CPUQuota=50%\nMemoryHigh=256M'
|
|
};
|
|
// Default install targets mirror systemd's usual system and user unit targets.
|
|
const systemdInstallTargets = {
|
|
system: {
|
|
service: 'multi-user.target',
|
|
timer: 'timers.target',
|
|
socket: 'sockets.target',
|
|
path: 'paths.target',
|
|
target: 'multi-user.target',
|
|
mount: 'local-fs.target',
|
|
automount: 'local-fs.target',
|
|
swap: 'swap.target',
|
|
slice: 'slices.target'
|
|
},
|
|
user: {
|
|
service: 'default.target',
|
|
timer: 'timers.target',
|
|
socket: 'sockets.target',
|
|
path: 'paths.target',
|
|
target: 'default.target',
|
|
mount: 'default.target',
|
|
automount: 'default.target',
|
|
swap: 'default.target',
|
|
slice: 'slices.target'
|
|
}
|
|
};
|
|
|
|
// Returns the currently selected type, falling back to the service form.
|
|
function currentUnitType()
|
|
{
|
|
const field = document.querySelector('select[name="unittype"]');
|
|
return field && field.value ? field.value : 'service';
|
|
}
|
|
|
|
// Updates generic placeholders to match the currently selected unit type.
|
|
function updateTypePlaceholders()
|
|
{
|
|
const type = currentUnitType();
|
|
const nameField = document.querySelector('input[name="name"]');
|
|
const descField = document.querySelector('input[name="desc"]');
|
|
if (nameField && systemdNamePlaceholders[type]) {
|
|
nameField.setAttribute('placeholder', systemdNamePlaceholders[type]);
|
|
}
|
|
if (descField && systemdDescPlaceholders[type]) {
|
|
descField.setAttribute('placeholder', systemdDescPlaceholders[type]);
|
|
}
|
|
const extraField = document.querySelector('textarea[name="unitconf"]');
|
|
if (extraField) {
|
|
extraField.setAttribute('placeholder',
|
|
systemdExtraPlaceholders[type] || '');
|
|
}
|
|
}
|
|
|
|
// Detects defaults we own, so a custom WantedBy value is not overwritten.
|
|
function knownInstallTarget(value)
|
|
{
|
|
for (const scope in systemdInstallTargets) {
|
|
for (const type in systemdInstallTargets[scope]) {
|
|
if (systemdInstallTargets[scope][type] == value) {
|
|
return true;
|
|
}
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
// Refreshes WantedBy only when it is blank or still one of our defaults.
|
|
function updateInstallTarget(userMode)
|
|
{
|
|
const field = document.querySelector('[name="wantedby"]');
|
|
if (!field) {
|
|
return;
|
|
}
|
|
const scope = userMode ? 'user' : 'system';
|
|
const target = systemdInstallTargets[scope][currentUnitType()];
|
|
if (target && (!field.value || knownInstallTarget(field.value))) {
|
|
field.value = target;
|
|
}
|
|
}
|
|
|
|
// Shows user-manager fields and hides service User=/Group= in user mode.
|
|
function userModeChange()
|
|
{
|
|
let checked = document.querySelector('input[name="userservice"]:checked');
|
|
const hidden = document.querySelector('input[name="userservice"][type="hidden"]');
|
|
const f = checked ? checked.form : null;
|
|
const userservice = f ? f.elements['userservice'] :
|
|
document.querySelectorAll('input[name="userservice"]');
|
|
if (!checked && userservice) {
|
|
for (let i = 0; i < userservice.length; i++) {
|
|
if (userservice[i].checked) {
|
|
checked = userservice[i];
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
const enabled = checked ? checked.value == '1' :
|
|
hidden ? hidden.value == '1' : false;
|
|
const service = currentUnitType() == 'service';
|
|
const socket = currentUnitType() == 'socket';
|
|
const showrow = function(id, show) {
|
|
const row = document.getElementById(id);
|
|
if (row) {
|
|
row.style.display = show ? '' : 'none';
|
|
}
|
|
};
|
|
showrow('systemd_unituser_row', enabled);
|
|
showrow('systemd_linger_row', enabled);
|
|
const userserviceHr =
|
|
document.querySelector('#systemd_userservice_row + tr');
|
|
if (userserviceHr) {
|
|
userserviceHr.style.display = enabled ? '' : 'none';
|
|
}
|
|
showrow('systemd_runas_user_row', !enabled && service);
|
|
showrow('systemd_runas_group_row', !enabled && service);
|
|
showrow('systemd_socket_user_row', !enabled && socket);
|
|
showrow('systemd_socket_group_row', !enabled && socket);
|
|
updateInstallTarget(enabled);
|
|
}
|
|
|
|
// Switches between service-specific rows and each unit type's guided fields.
|
|
function unitTypeChange()
|
|
{
|
|
const type = currentUnitType();
|
|
const service = type == 'service';
|
|
const extra = !service && type != 'target';
|
|
const mount = type == 'mount';
|
|
const automount = type == 'automount';
|
|
const typedRowSets = {
|
|
timer: document.querySelectorAll('[data-systemd-timer]'),
|
|
socket: document.querySelectorAll('[data-systemd-socket]'),
|
|
path: document.querySelectorAll('[data-systemd-path]'),
|
|
mount: document.querySelectorAll('[data-systemd-mount]'),
|
|
automount: document.querySelectorAll('[data-systemd-automount]'),
|
|
swap: document.querySelectorAll('[data-systemd-swap]'),
|
|
slice: document.querySelectorAll('[data-systemd-slice]')
|
|
};
|
|
const suffix = document.getElementById('systemd_name_suffix');
|
|
if (suffix) {
|
|
suffix.textContent = systemdSuffixes[type] || '';
|
|
}
|
|
updateTypePlaceholders();
|
|
const serviceRows = document.querySelectorAll('[data-systemd-service]');
|
|
for (let i = 0; i < serviceRows.length; i++) {
|
|
serviceRows[i].style.display = service ? '' : 'none';
|
|
}
|
|
for (const rowType in typedRowSets) {
|
|
const rows = typedRowSets[rowType];
|
|
for (let i = 0; i < rows.length; i++) {
|
|
rows[i].style.display = type == rowType ? '' : 'none';
|
|
}
|
|
}
|
|
const extraRows = document.querySelectorAll('[data-systemd-extra]');
|
|
for (let i = 0; i < extraRows.length; i++) {
|
|
extraRows[i].style.display = extra ? '' : 'none';
|
|
}
|
|
userModeChange();
|
|
}
|
|
|
|
// Authentic and Gray themes can render rows at different times, so initialize
|
|
// after DOM readiness and also bind explicit change handlers.
|
|
function initializeSystemdUnitForm()
|
|
{
|
|
const systemdUserServiceInputs =
|
|
document.querySelectorAll('input[name="userservice"]');
|
|
for (let i = 0; i < systemdUserServiceInputs.length; i++) {
|
|
systemdUserServiceInputs[i].addEventListener('change',
|
|
userModeChange);
|
|
}
|
|
const systemdUnitTypeInput = document.querySelector('select[name="unittype"]');
|
|
if (systemdUnitTypeInput) {
|
|
systemdUnitTypeInput.addEventListener('change',
|
|
unitTypeChange);
|
|
}
|
|
unitTypeChange();
|
|
}
|
|
|
|
if (document.readyState == 'loading') {
|
|
document.addEventListener('DOMContentLoaded', initializeSystemdUnitForm);
|
|
}
|
|
else {
|
|
initializeSystemdUnitForm();
|
|
}
|
|
})();
|
|
EOF
|
|
print ui_tag('script', $systemd_js,
|
|
{ 'type' => 'text/javascript' });
|
|
}
|
|
else {
|
|
# Existing units are edited as raw files to preserve unknown directives.
|
|
print ui_table_start($text{'systemd_header'}, undef, 2);
|
|
|
|
# Unit names are identifiers and cannot be renamed from the edit page.
|
|
print ui_table_row(hlink($text{'systemd_name'}, "systemd_name"),
|
|
ui_tag('tt', html_escape($in{'name'})));
|
|
|
|
# Show the resolved file path before the editable unit contents.
|
|
my $edit_file = $u->{'file'};
|
|
if ($edit_dropin) {
|
|
$edit_file = $dropin_file ||
|
|
($edit_user_scope ?
|
|
user_dropin_file($unituser, $in{'name'}) :
|
|
system_dropin_file($in{'name'}));
|
|
$edit_file || error($text{'systemd_edropinfile'});
|
|
}
|
|
print ui_table_row(hlink($text{'systemd_file'}, "systemd_file"),
|
|
ui_tag('tt', html_escape($edit_file)));
|
|
|
|
# User files are read through privilege-dropping helpers so a path in the
|
|
# home tree cannot make root follow user-controlled symlinks.
|
|
if ($edit_dropin) {
|
|
$conf = $dropin_file && $edit_user_scope ?
|
|
read_user_dropin_config_file($unituser, $dropin_file) :
|
|
$dropin_file ?
|
|
read_system_dropin_config_file($dropin_file) :
|
|
$edit_user_scope ?
|
|
read_user_dropin_file($unituser, $in{'name'}) :
|
|
read_system_dropin_file($in{'name'});
|
|
defined($conf) || error($text{'systemd_edropinfile'});
|
|
}
|
|
else {
|
|
$conf = $edit_user_scope ?
|
|
read_user_unit_file($unituser, $u->{'file'}) :
|
|
read_file_contents($u->{'file'});
|
|
defined($conf) || error($text{'systemd_euserunitfile'});
|
|
}
|
|
print ui_table_row(hlink($text{'systemd_conf'}, "systemd_conf"),
|
|
ui_textarea("data", $conf, 20, 80, undef,
|
|
undef, $unit_file_editable &&
|
|
$can_save_unit ? undef :
|
|
"readonly='readonly'"));
|
|
|
|
if ($edit_user_scope) {
|
|
# The owner is fixed for an existing user unit.
|
|
print ui_table_row(hlink($text{'systemd_unituser'}, "systemd_unituser"),
|
|
ui_tag('tt', html_escape($unituser)));
|
|
}
|
|
|
|
# Show systemd's own state model before editable policy toggles.
|
|
print ui_table_row(hlink($text{'systemd_runtime_state'},
|
|
"systemd_runtime_state"),
|
|
edit_runtime_state($u->{'runtime'}, $u->{'substate'}));
|
|
if (defined($u->{'pid'}) && $u->{'pid'} =~ /^\d+$/ && $u->{'pid'} > 0) {
|
|
print ui_table_row(hlink($text{'systemd_main_pid'},
|
|
"systemd_main_pid"),
|
|
ui_tag('tt', html_escape($u->{'pid'})));
|
|
}
|
|
print ui_table_row(hlink($text{'systemd_unit_state'},
|
|
"systemd_unit_state"),
|
|
edit_state_value($u->{'unitstate'}));
|
|
|
|
# Only file-backed installable units can have their startup state changed.
|
|
if (boot_state_changeable($u->{'unitstate'}, $u->{'name'}) &&
|
|
systemd_can_boot(\%access, $edit_user_scope, $unituser)) {
|
|
print ui_table_row(hlink($text{'systemd_boot'}, "systemd_boot"),
|
|
ui_yesno_radio("boot", $u->{'boot'}));
|
|
}
|
|
|
|
# User-scope edits allow linger to be managed alongside the raw unit file.
|
|
if ($edit_user_scope) {
|
|
my $linger_enabled = user_linger_enabled($unituser);
|
|
my $linger_field = systemd_can_linger(\%access, $unituser) ?
|
|
ui_yesno_radio("linger", $linger_enabled) :
|
|
html_escape($linger_enabled ? $text{'yes'} : $text{'no'});
|
|
print ui_table_row(hlink($text{'systemd_linger_user'},
|
|
"systemd_linger_user"),
|
|
$linger_field);
|
|
}
|
|
|
|
print ui_table_end();
|
|
}
|
|
|
|
if ($in{'new'}) {
|
|
# New units only need a create button; runtime actions appear after save.
|
|
print ui_form_end([ [ undef, $text{'create'} ] ]);
|
|
}
|
|
else {
|
|
# Keep save, override, runtime and inspection actions in nearby clusters;
|
|
# destructive actions stay isolated on the far side of the button row.
|
|
my @save_buttons = $unit_file_editable && $can_save_unit ?
|
|
( [ undef, $text{'save'} ] ) : ( );
|
|
my @control_buttons;
|
|
my @inspect_buttons = systemd_can_inspect(
|
|
\%access, $edit_user_scope, $unituser) ?
|
|
( [ 'status', $text{'edit_statusnow'} ],
|
|
[ 'props', $text{'edit_propsnow'} ],
|
|
[ 'deps', $text{'edit_depsnow'} ] ) : ( );
|
|
my @log_buttons = systemd_can_logs(
|
|
\%access, $edit_user_scope, $unituser) ?
|
|
( [ 'logs', $text{'edit_logsnow'} ] ) : ( );
|
|
|
|
# Running units can be stopped, but only restart units where systemd
|
|
# supports a restart job type. Some runtime units, such as scopes and
|
|
# devices, are externally managed and can only be inspected or stopped.
|
|
if (defined($u->{'status'}) && $u->{'status'} == 1) {
|
|
push(@control_buttons, [ 'restart', $text{'edit_restartnow'} ])
|
|
if (unit_restartable($in{'name'}) &&
|
|
systemd_can_runtime(
|
|
\%access, 'restart', $edit_user_scope,
|
|
$unituser));
|
|
push(@control_buttons, [ 'stop', $text{'edit_stopnow'} ])
|
|
if (systemd_can_runtime(
|
|
\%access, 'stop', $edit_user_scope,
|
|
$unituser));
|
|
}
|
|
elsif (unit_startable($in{'name'}) &&
|
|
systemd_can_runtime(\%access, 'start',
|
|
$edit_user_scope, $unituser)) {
|
|
push(@control_buttons, [ 'start', $text{'edit_startnow'} ]);
|
|
}
|
|
|
|
my @override_buttons;
|
|
if ($edit_dropin) {
|
|
push(@override_buttons,
|
|
[ 'stock_unit',
|
|
$text{'edit_stockunitnow'} || "Stock Unit" ]);
|
|
}
|
|
elsif ($unit_file_editable &&
|
|
systemd_can_dropin(\%access, $edit_user_scope, $unituser)) {
|
|
my $override_text = dropin_exists($edit_user_scope,
|
|
$unituser, $in{'name'}) ?
|
|
($text{'edit_editoverridenow'} ||
|
|
"Edit Override") :
|
|
($text{'edit_overridenow'} ||
|
|
"Create Override");
|
|
push(@override_buttons, [ 'override', $override_text ]);
|
|
}
|
|
my @delete_buttons;
|
|
if ($edit_dropin && !$dropin_file && $unit_file_editable &&
|
|
systemd_can_dropin(\%access, $edit_user_scope, $unituser)) {
|
|
push(@delete_buttons,
|
|
[ 'delete_override',
|
|
$text{'edit_deleteoverridenow'} || "Delete Override" ]);
|
|
}
|
|
elsif ($unit_file_editable && $in{'name'} ne 'webmin.service' &&
|
|
systemd_can_delete(\%access, $edit_user_scope, $unituser)) {
|
|
push(@delete_buttons, [ 'delete', $text{'delete'} ]);
|
|
}
|
|
|
|
print ui_form_grouped_buttons([ [ \@save_buttons,
|
|
\@override_buttons,
|
|
\@control_buttons,
|
|
\@inspect_buttons,
|
|
\@log_buttons ],
|
|
[ \@delete_buttons ] ]);
|
|
print ui_form_end();
|
|
}
|
|
|
|
# Return to the index tab that owns this unit when the type or scope is known.
|
|
my $footer_url = $in{'new'} ?
|
|
index_url(".".$default_unittype, $create_user_scope, $unituser) :
|
|
index_url($in{'name'}, $edit_user_scope, $unituser);
|
|
ui_print_footer($footer_url, $text{'index_return'});
|
|
|
|
# edit_runtime_state(active-state, sub-state)
|
|
# Returns a systemd-style runtime state value such as "Active (running)".
|
|
sub edit_runtime_state
|
|
{
|
|
my ($state, $substate) = @_;
|
|
my $value = edit_state_value($state);
|
|
if (defined($state) && $state ne "" &&
|
|
defined($substate) && $substate ne "" && $substate ne $state) {
|
|
$value .= " ".ui_tag('span',
|
|
"(".html_escape(lcfirst($substate)).")");
|
|
}
|
|
return $value;
|
|
}
|
|
|
|
# edit_state_value(state)
|
|
# Returns a formatted systemd state value for the edit form.
|
|
sub edit_state_value
|
|
{
|
|
my ($state) = @_;
|
|
return ui_tag('i', $text{'index_unknown'})
|
|
if (!defined($state) || $state eq "");
|
|
return html_escape(ucfirst($state));
|
|
}
|