Files
nixpkgs/nixos/tests/systemd-confinement/concurrent-runner.py
aszlig 270601f6c4 nixos/systemd-confinement: Fix with template units
Quoting from <https://github.com/NixOS/nixpkgs/issues/464323>:

> When using confinement.enable = true for an instanced systemd service,
> the 2nd instance will fail to start if the 1st instance is still
> running.
>
> This only happens with confinement.enable = true;. Removing this
> option causes both service instances to succeed. Maybe this happens
> because the /run/confinement/fortune directory is shared between the
> instances.

The reason why this happens is that the root directory is set to
"/run/confinement/${mkPathSafeName name}", which is the non-expanded
unit name rather than the full unit name with the instance in case of a
template unit.

So when a template unit "foo@.service" is involved, the root directory
is then "/run/confinement/foo_" regardless of instance, so
foo@bar.service uses the same directory as foo@baz.service and when the
first unit cleans up the root directory, it also makes it inaccessible
for the unit started afterwards.

I added a small property test to test concurrent invocations, so we
cover this case and other issues that might come up with template units
in a future refactor.

Signed-off-by: aszlig <aszlig@nix.build>
2025-12-05 16:52:08 +01:00

132 lines
4.0 KiB
Python

import click
import socket
import sys
from hypothesis import given, settings, strategies as st
from subprocess import run
from time import sleep
@st.composite
def client_actions(draw, size: int = 10):
"""
Generate a string describing a set of actions to perform.
This is specifically "stringly-typed" so that when looking at the output of
a failed test run, it's easy to visually identify what's wrong.
The string may consist of the following characters:
' ' - Sleep for one tick (0.1s)
'[' - Start the client
']' - Stop the client
'R' - Run a subprocess in the client
So for example the string " [ R ] " would mean:
* Sleep for two ticks (" ")
* Start the client ("[")
* Sleep for two ticks (" ")
* Run the subprocess ("R")
* Sleep for one tick (" ")
* Stop the client ("]")
* Sleep for two ticks (" ")
Exactly the same encoding as above is used for the network protocol, so for
debugging issues, all you need to know is the representation above.
"""
assert size > 1
start = None
stop = None
runs = set()
if draw(st.booleans()):
start = draw(st.integers(min_value=0, max_value=size - 2))
stop = draw(st.integers(min_value=start + 1, max_value=size - 1))
if start + 1 < stop:
runs = draw(st.sets(
st.integers(min_value=start + 1, max_value=stop - 1),
max_size=stop - start,
))
out = ''
for index in range(size):
if start is not None and index == start:
out += '['
elif stop is not None and index == stop:
out += ']'
elif index in runs:
out += 'R'
else:
out += ' '
return out
@click.group()
def cli() -> None:
pass
@cli.command('driver')
@settings(deadline=None, max_examples=20)
@given(st.lists(client_actions(), max_size=5))
def test_driver(client_actions: list[str]) -> None:
clients: list[None | socket.socket] = [None] * len(client_actions)
for index in range(max(map(len, client_actions), default=0)):
for n, actions in enumerate(client_actions):
client = clients[n]
try:
action = actions[index]
except IndexError:
continue
match action:
case '[':
client = socket.socket(socket.AF_INET6)
client.settimeout(60)
client.connect(('::1', 12345))
client.send(b'[')
clients[n] = client
case ']':
assert client is not None
client.send(b']')
# At this point if we get ']' back from the client, we know
# that everything went smoothly up to this point because
# otherwise the client would have just thrown an exception
# and the connection would be closed.
assert client.recv(1) == b']'
assert not client.recv(1)
client.close()
clients[n] = None
case 'R':
assert client is not None
client.send(b'R')
case ' ':
if client is not None:
client.send(b' ')
sleep(0.1)
assert all(c is None for c in clients), \
f'clients still running: {clients!r}'
@cli.command('client')
@click.argument('executable')
def test_client(executable: str) -> None:
if not (action := sys.stdin.read(1)):
raise SystemExit(1)
assert action == '[', f'{action!r} != "["'
while action := sys.stdin.read(1):
match action:
case 'R':
run([executable], check=True, stdout=sys.stderr)
case ']':
sys.stdout.write(']')
return
case ' ':
sleep(0.1)
case '':
raise SystemExit(1)
if __name__ == '__main__':
cli()