Merge pull request #5354 from msekletar/issue-518
service: serialize information about currently executing command
This commit is contained in:
commit
8ea9aa9e88
|
@ -5946,7 +5946,8 @@ EXTRA_DIST += \
|
|||
src/network/systemd-networkd.pkla \
|
||||
units/systemd-networkd.service.m4.in \
|
||||
units/systemd-networkd-wait-online.service.in \
|
||||
test/networkd-test.py
|
||||
test/networkd-test.py \
|
||||
test/test-exec-deserialization.py
|
||||
|
||||
# ------------------------------------------------------------------------------
|
||||
if ENABLE_LOGIND
|
||||
|
|
|
@ -45,6 +45,7 @@
|
|||
#include "service.h"
|
||||
#include "signal-util.h"
|
||||
#include "special.h"
|
||||
#include "stdio-util.h"
|
||||
#include "string-table.h"
|
||||
#include "string-util.h"
|
||||
#include "strv.h"
|
||||
|
@ -2140,6 +2141,80 @@ _pure_ static bool service_can_reload(Unit *u) {
|
|||
return !!s->exec_command[SERVICE_EXEC_RELOAD];
|
||||
}
|
||||
|
||||
static unsigned service_exec_command_index(Unit *u, ServiceExecCommand id, ExecCommand *current) {
|
||||
Service *s = SERVICE(u);
|
||||
unsigned idx = 0;
|
||||
ExecCommand *first, *c;
|
||||
|
||||
assert(s);
|
||||
|
||||
first = s->exec_command[id];
|
||||
|
||||
/* Figure out where we are in the list by walking back to the beginning */
|
||||
for (c = current; c != first; c = c->command_prev)
|
||||
idx++;
|
||||
|
||||
return idx;
|
||||
}
|
||||
|
||||
static int service_serialize_exec_command(Unit *u, FILE *f, ExecCommand *command) {
|
||||
Service *s = SERVICE(u);
|
||||
ServiceExecCommand id;
|
||||
unsigned idx;
|
||||
const char *type;
|
||||
char **arg;
|
||||
_cleanup_strv_free_ char **escaped_args = NULL;
|
||||
_cleanup_free_ char *args = NULL, *p = NULL;
|
||||
size_t allocated = 0, length = 0;
|
||||
|
||||
assert(s);
|
||||
assert(f);
|
||||
|
||||
if (!command)
|
||||
return 0;
|
||||
|
||||
if (command == s->control_command) {
|
||||
type = "control";
|
||||
id = s->control_command_id;
|
||||
} else {
|
||||
type = "main";
|
||||
id = SERVICE_EXEC_START;
|
||||
}
|
||||
|
||||
idx = service_exec_command_index(u, id, command);
|
||||
|
||||
STRV_FOREACH(arg, command->argv) {
|
||||
size_t n;
|
||||
_cleanup_free_ char *e = NULL;
|
||||
|
||||
e = xescape(*arg, WHITESPACE);
|
||||
if (!e)
|
||||
return -ENOMEM;
|
||||
|
||||
n = strlen(e);
|
||||
if (!GREEDY_REALLOC(args, allocated, length + 1 + n + 1))
|
||||
return -ENOMEM;
|
||||
|
||||
if (length > 0)
|
||||
args[length++] = ' ';
|
||||
|
||||
memcpy(args + length, e, n);
|
||||
length += n;
|
||||
}
|
||||
|
||||
if (!GREEDY_REALLOC(args, allocated, length + 1))
|
||||
return -ENOMEM;
|
||||
args[length++] = 0;
|
||||
|
||||
p = xescape(command->path, WHITESPACE);
|
||||
if (!p)
|
||||
return -ENOMEM;
|
||||
|
||||
fprintf(f, "%s-command=%s %u %s %s\n", type, service_exec_command_to_string(id), idx, p, args);
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int service_serialize(Unit *u, FILE *f, FDSet *fds) {
|
||||
Service *s = SERVICE(u);
|
||||
ServiceFDStore *fs;
|
||||
|
@ -2167,11 +2242,8 @@ static int service_serialize(Unit *u, FILE *f, FDSet *fds) {
|
|||
if (r < 0)
|
||||
return r;
|
||||
|
||||
/* FIXME: There's a minor uncleanliness here: if there are
|
||||
* multiple commands attached here, we will start from the
|
||||
* first one again */
|
||||
if (s->control_command_id >= 0)
|
||||
unit_serialize_item(u, f, "control-command", service_exec_command_to_string(s->control_command_id));
|
||||
service_serialize_exec_command(u, f, s->control_command);
|
||||
service_serialize_exec_command(u, f, s->main_command);
|
||||
|
||||
r = unit_serialize_item_fd(u, f, fds, "stdin-fd", s->stdin_fd);
|
||||
if (r < 0)
|
||||
|
@ -2227,6 +2299,106 @@ static int service_serialize(Unit *u, FILE *f, FDSet *fds) {
|
|||
return 0;
|
||||
}
|
||||
|
||||
static int service_deserialize_exec_command(Unit *u, const char *key, const char *value) {
|
||||
Service *s = SERVICE(u);
|
||||
int r;
|
||||
unsigned idx = 0, i;
|
||||
bool control, found = false;
|
||||
ServiceExecCommand id = _SERVICE_EXEC_COMMAND_INVALID;
|
||||
ExecCommand *command = NULL;
|
||||
_cleanup_free_ char *args = NULL, *path = NULL;
|
||||
_cleanup_strv_free_ char **argv = NULL;
|
||||
|
||||
enum ExecCommandState {
|
||||
STATE_EXEC_COMMAND_TYPE,
|
||||
STATE_EXEC_COMMAND_INDEX,
|
||||
STATE_EXEC_COMMAND_PATH,
|
||||
STATE_EXEC_COMMAND_ARGS,
|
||||
_STATE_EXEC_COMMAND_MAX,
|
||||
_STATE_EXEC_COMMAND_INVALID = -1,
|
||||
} state;
|
||||
|
||||
assert(s);
|
||||
assert(key);
|
||||
assert(value);
|
||||
|
||||
control = streq(key, "control-command");
|
||||
|
||||
state = STATE_EXEC_COMMAND_TYPE;
|
||||
|
||||
for (;;) {
|
||||
_cleanup_free_ char *arg = NULL;
|
||||
|
||||
r = extract_first_word(&value, &arg, NULL, EXTRACT_CUNESCAPE);
|
||||
if (r == 0)
|
||||
break;
|
||||
else if (r < 0)
|
||||
return r;
|
||||
|
||||
switch (state) {
|
||||
case STATE_EXEC_COMMAND_TYPE:
|
||||
id = service_exec_command_from_string(arg);
|
||||
if (id < 0)
|
||||
return -EINVAL;
|
||||
|
||||
state = STATE_EXEC_COMMAND_INDEX;
|
||||
break;
|
||||
case STATE_EXEC_COMMAND_INDEX:
|
||||
r = safe_atou(arg, &idx);
|
||||
if (r < 0)
|
||||
return -EINVAL;
|
||||
|
||||
state = STATE_EXEC_COMMAND_PATH;
|
||||
break;
|
||||
case STATE_EXEC_COMMAND_PATH:
|
||||
path = arg;
|
||||
arg = NULL;
|
||||
state = STATE_EXEC_COMMAND_ARGS;
|
||||
|
||||
if (!path_is_absolute(path))
|
||||
return -EINVAL;
|
||||
break;
|
||||
case STATE_EXEC_COMMAND_ARGS:
|
||||
r = strv_extend(&argv, arg);
|
||||
if (r < 0)
|
||||
return -ENOMEM;
|
||||
break;
|
||||
default:
|
||||
assert_not_reached("Unknown error at deserialization of exec command");
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (state != STATE_EXEC_COMMAND_ARGS)
|
||||
return -EINVAL;
|
||||
|
||||
/* Let's check whether exec command on given offset matches data that we just deserialized */
|
||||
for (command = s->exec_command[id], i = 0; command; command = command->command_next, i++) {
|
||||
if (i != idx)
|
||||
continue;
|
||||
|
||||
found = strv_equal(argv, command->argv) && streq(command->path, path);
|
||||
break;
|
||||
}
|
||||
|
||||
if (!found) {
|
||||
/* Command at the index we serialized is different, let's look for command that exactly
|
||||
* matches but is on different index. If there is no such command we will not resume execution. */
|
||||
for (command = s->exec_command[id]; command; command = command->command_next)
|
||||
if (strv_equal(command->argv, argv) && streq(command->path, path))
|
||||
break;
|
||||
}
|
||||
|
||||
if (command && control)
|
||||
s->control_command = command;
|
||||
else if (command)
|
||||
s->main_command = command;
|
||||
else
|
||||
log_unit_warning(u, "Current command vanished from the unit file, execution of the command list won't be resumed.");
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int service_deserialize_item(Unit *u, const char *key, const char *value, FDSet *fds) {
|
||||
Service *s = SERVICE(u);
|
||||
int r;
|
||||
|
@ -2309,16 +2481,6 @@ static int service_deserialize_item(Unit *u, const char *key, const char *value,
|
|||
s->status_text = t;
|
||||
}
|
||||
|
||||
} else if (streq(key, "control-command")) {
|
||||
ServiceExecCommand id;
|
||||
|
||||
id = service_exec_command_from_string(value);
|
||||
if (id < 0)
|
||||
log_unit_debug(u, "Failed to parse exec-command value: %s", value);
|
||||
else {
|
||||
s->control_command_id = id;
|
||||
s->control_command = s->exec_command[id];
|
||||
}
|
||||
} else if (streq(key, "accept-socket")) {
|
||||
Unit *socket;
|
||||
|
||||
|
@ -2437,6 +2599,10 @@ static int service_deserialize_item(Unit *u, const char *key, const char *value,
|
|||
s->watchdog_override_enable = true;
|
||||
s->watchdog_override_usec = watchdog_override_usec;
|
||||
}
|
||||
} else if (STR_IN_SET(key, "main-command", "control-command")) {
|
||||
r = service_deserialize_exec_command(u, key, value);
|
||||
if (r < 0)
|
||||
log_unit_debug_errno(u, r, "Failed to parse serialized command \"%s\": %m", value);
|
||||
} else
|
||||
log_unit_debug(u, "Unknown serialization key: %s", key);
|
||||
|
||||
|
|
|
@ -0,0 +1,192 @@
|
|||
#!/usr/bin/python3
|
||||
|
||||
#
|
||||
# Copyright 2017 Michal Sekletar <msekleta@redhat.com>
|
||||
#
|
||||
# systemd is free software; you can redistribute it and/or modify it
|
||||
# under the terms of the GNU Lesser General Public License as published by
|
||||
# the Free Software Foundation; either version 2.1 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# systemd is distributed in the hope that it will be useful, but
|
||||
# WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
||||
# Lesser General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU Lesser General Public License
|
||||
# along with systemd; If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
# ATTENTION: This uses the *installed* systemd, not the one from the built
|
||||
# source tree.
|
||||
|
||||
import unittest
|
||||
import time
|
||||
import os
|
||||
import tempfile
|
||||
import subprocess
|
||||
|
||||
from enum import Enum
|
||||
|
||||
class UnitFileChange(Enum):
|
||||
NO_CHANGE = 0
|
||||
LINES_SWAPPED = 1
|
||||
COMMAND_ADDED_BEFORE = 2
|
||||
COMMAND_ADDED_AFTER = 3
|
||||
COMMAND_INTERLEAVED = 4
|
||||
REMOVAL = 5
|
||||
|
||||
class ExecutionResumeTest(unittest.TestCase):
|
||||
def setUp(self):
|
||||
self.unit = 'test-issue-518.service'
|
||||
self.unitfile_path = '/run/systemd/system/{0}'.format(self.unit)
|
||||
self.output_file = tempfile.mktemp()
|
||||
self.unit_files = {}
|
||||
|
||||
unit_file_content = '''
|
||||
[Service]
|
||||
Type=oneshot
|
||||
ExecStart=/bin/sleep 2
|
||||
ExecStart=/bin/bash -c "echo foo >> {0}"
|
||||
'''.format(self.output_file)
|
||||
self.unit_files[UnitFileChange.NO_CHANGE] = unit_file_content
|
||||
|
||||
unit_file_content = '''
|
||||
[Service]
|
||||
Type=oneshot
|
||||
ExecStart=/bin/bash -c "echo foo >> {0}"
|
||||
ExecStart=/bin/sleep 2
|
||||
'''.format(self.output_file)
|
||||
self.unit_files[UnitFileChange.LINES_SWAPPED] = unit_file_content
|
||||
|
||||
unit_file_content = '''
|
||||
[Service]
|
||||
Type=oneshot
|
||||
ExecStart=/bin/bash -c "echo bar >> {0}"
|
||||
ExecStart=/bin/sleep 2
|
||||
ExecStart=/bin/bash -c "echo foo >> {0}"
|
||||
'''.format(self.output_file)
|
||||
self.unit_files[UnitFileChange.COMMAND_ADDED_BEFORE] = unit_file_content
|
||||
|
||||
unit_file_content = '''
|
||||
[Service]
|
||||
Type=oneshot
|
||||
ExecStart=/bin/sleep 2
|
||||
ExecStart=/bin/bash -c "echo foo >> {0}"
|
||||
ExecStart=/bin/bash -c "echo bar >> {0}"
|
||||
'''.format(self.output_file)
|
||||
self.unit_files[UnitFileChange.COMMAND_ADDED_AFTER] = unit_file_content
|
||||
|
||||
unit_file_content = '''
|
||||
[Service]
|
||||
Type=oneshot
|
||||
ExecStart=/bin/bash -c "echo baz >> {0}"
|
||||
ExecStart=/bin/sleep 2
|
||||
ExecStart=/bin/bash -c "echo foo >> {0}"
|
||||
ExecStart=/bin/bash -c "echo bar >> {0}"
|
||||
'''.format(self.output_file)
|
||||
self.unit_files[UnitFileChange.COMMAND_INTERLEAVED] = unit_file_content
|
||||
|
||||
unit_file_content = '''
|
||||
[Service]
|
||||
Type=oneshot
|
||||
ExecStart=/bin/bash -c "echo bar >> {0}"
|
||||
ExecStart=/bin/bash -c "echo baz >> {0}"
|
||||
'''.format(self.output_file)
|
||||
self.unit_files[UnitFileChange.REMOVAL] = unit_file_content
|
||||
|
||||
def reload(self):
|
||||
subprocess.check_call(['systemctl', 'daemon-reload'])
|
||||
|
||||
def write_unit_file(self, unit_file_change):
|
||||
if not isinstance(unit_file_change, UnitFileChange):
|
||||
raise ValueError('Unknown unit file change')
|
||||
|
||||
content = self.unit_files[unit_file_change]
|
||||
|
||||
with open(self.unitfile_path, 'w') as f:
|
||||
f.write(content)
|
||||
|
||||
self.reload()
|
||||
|
||||
def check_output(self, expected_output):
|
||||
try:
|
||||
with open(self.output_file, 'r') as log:
|
||||
output = log.read()
|
||||
except IOError:
|
||||
self.fail()
|
||||
|
||||
self.assertEqual(output, expected_output)
|
||||
|
||||
def setup_unit(self):
|
||||
self.write_unit_file(UnitFileChange.NO_CHANGE)
|
||||
subprocess.check_call(['systemctl', '--job-mode=replace', '--no-block', 'start', self.unit])
|
||||
|
||||
def test_no_change(self):
|
||||
expected_output = 'foo\n'
|
||||
|
||||
self.setup_unit()
|
||||
self.reload()
|
||||
time.sleep(4)
|
||||
|
||||
self.check_output(expected_output)
|
||||
|
||||
def test_swapped(self):
|
||||
expected_output = ''
|
||||
|
||||
self.setup_unit()
|
||||
self.write_unit_file(UnitFileChange.LINES_SWAPPED)
|
||||
self.reload()
|
||||
time.sleep(4)
|
||||
|
||||
self.assertTrue(not os.path.exists(self.output_file))
|
||||
|
||||
def test_added_before(self):
|
||||
expected_output = 'foo\n'
|
||||
|
||||
self.setup_unit()
|
||||
self.write_unit_file(UnitFileChange.COMMAND_ADDED_BEFORE)
|
||||
self.reload()
|
||||
time.sleep(4)
|
||||
|
||||
self.check_output(expected_output)
|
||||
|
||||
def test_added_after(self):
|
||||
expected_output = 'foo\nbar\n'
|
||||
|
||||
self.setup_unit()
|
||||
self.write_unit_file(UnitFileChange.COMMAND_ADDED_AFTER)
|
||||
self.reload()
|
||||
time.sleep(4)
|
||||
|
||||
self.check_output(expected_output)
|
||||
|
||||
def test_interleaved(self):
|
||||
expected_output = 'foo\nbar\n'
|
||||
|
||||
self.setup_unit()
|
||||
self.write_unit_file(UnitFileChange.COMMAND_INTERLEAVED)
|
||||
self.reload()
|
||||
time.sleep(4)
|
||||
|
||||
self.check_output(expected_output)
|
||||
|
||||
def test_removal(self):
|
||||
self.setup_unit()
|
||||
self.write_unit_file(UnitFileChange.REMOVAL)
|
||||
self.reload()
|
||||
time.sleep(4)
|
||||
|
||||
self.assertTrue(not os.path.exists(self.output_file))
|
||||
|
||||
def tearDown(self):
|
||||
for f in [self.output_file, self.unitfile_path]:
|
||||
try:
|
||||
os.remove(f)
|
||||
except OSError:
|
||||
# ignore error if log file doesn't exist
|
||||
pass
|
||||
|
||||
self.reload()
|
||||
|
||||
if __name__ == '__main__':
|
||||
unittest.main()
|
Loading…
Reference in New Issue