summaryrefslogtreecommitdiff
path: root/test/selfmount.py
diff options
context:
space:
mode:
Diffstat (limited to 'test/selfmount.py')
-rw-r--r--test/selfmount.py115
1 files changed, 114 insertions, 1 deletions
diff --git a/test/selfmount.py b/test/selfmount.py
index 0111e6ac..61d32523 100644
--- a/test/selfmount.py
+++ b/test/selfmount.py
@@ -256,6 +256,53 @@ def run(binary):
time.sleep(.05)
check(got == '0' and all(f' held-{i}\n' in log for i in range(1, 41)),
f'40 closes holding a last line each through the mount, all answered, every line run ({got!r})')
+ # An Edit whose filter writes the very ctl the Edit came by: 9ns
+ # opens with FOPEN_PARALLEL_DIRECT_WRITES, so the filter's write
+ # is not queued behind the one waiting for it. (Its own open may
+ # not truncate, `1<>`, not `>`: Linux takes the file's lock
+ # outright for an O_TRUNC open, and the waiting write holds it
+ # shared; nothing a server answers can change that.)
+ started = time.monotonic()
+ got = run(f"printf 'Edit ,>echo Msg filter-hi 1<> {mount}/pane/{scratch}/ctl\\n' > {mount}/pane/{scratch}/ctl\n".encode())
+ took = time.monotonic() - started
+ check(got == '0' and took < 5 and 'filter-hi' in client.read('/log').decode(),
+ f'an Edit filter writing its own ctl through the mount answers promptly ({got!r}, {took:.1f}s)')
+ # Another session dies under a burst of reads of it through the
+ # registry mount: every read returns (an error is fine), none
+ # hangs. 9ns invalidates the dead entry from a thread of its own,
+ # never the one that must answer the reads holding its parent.
+ other = name + 'k'
+ other_sock = f'{runtime}/pardes-9p-{other}.sock'
+ other_env = dict(env, PARDES_DUMP=f'/tmp/{other}.dump.zon')
+ other_pid, other_master = pty.fork()
+ if other_pid == 0:
+ os.execve(binary, [binary, '--tty', '--9p=' + other], other_env)
+ os.set_blocking(other_master, False)
+ deadline = time.monotonic() + 15
+ while not os.path.exists(other_sock) and time.monotonic() < deadline:
+ try:
+ os.read(other_master, 65536)
+ except OSError:
+ pass
+ time.sleep(.05)
+ codes = Path(f'/tmp/{name}.codes')
+ codes.unlink(missing_ok=True)
+
+ def kill_other():
+ time.sleep(1.5)
+ os.kill(other_pid, signal.SIGKILL)
+ killer = threading.Thread(target=kill_other, daemon=True)
+ killer.start()
+ ran = run((f"for k in 1 2 3 4 5; do (end=$((SECONDS+4)); while [ $SECONDS -lt $end ]; do "
+ f"timeout 10 cat /mnt/9p/pardes/{other}/index > /dev/null 2>&1; echo $? >> {codes}; done) & done; wait\n").encode())
+ killer.join(5)
+ os.waitpid(other_pid, 0)
+ got_codes = codes.read_text().split() if codes.exists() else []
+ codes.unlink(missing_ok=True)
+ for path in (other_sock, f'/tmp/{other}.dump.zon'):
+ Path(path).unlink(missing_ok=True)
+ check(ran == '0' and got_codes and '124' not in got_codes,
+ f'reads of a session killed under them all return ({ran!r}, {len(got_codes)} reads, {got_codes.count("124")} hung)')
# Forty event reads held through the mount, a follower each, and
# the mount still answers the rest: 9ns keeps workers past what
# the editor holds (128), so `cat layout` is not queued behind
@@ -291,9 +338,75 @@ def run(binary):
said_many = many.read_text() if many.exists() else ''
many.unlink(missing_ok=True)
check(ran == '9' and 'Too many open files' in said_many, f'an open past the 64 records is EMFILE ({ran!r}, {said_many[-80:]!r})')
+ # An Edit whose command runs is held, not waited on: through the
+ # mount, a status read beside a slow one answers at once, and a
+ # command that reads this session's own tree runs.
+ edited = int(client.read('/pane/new'))
+ client.write(f'/pane/{edited}/body', b'pear\napple\n')
+ took = Path(f'/tmp/{name}.took')
+ ran = run((f"echo 'Edit , | sleep 3; sort' > {mount}/pane/{edited}/ctl & sleep .5; "
+ f"s=$(date +%s%N); timeout 5 cat {mount}/status > /dev/null; e=$(date +%s%N); "
+ f"echo $(( (e - s) / 1000000 )) > {took}; wait\n").encode())
+ ms = int(took.read_text()) if took.exists() else -1
+ took.unlink(missing_ok=True)
+ check(ran == '0' and 0 <= ms < 1000, f'a status read beside a slow Edit answers at once ({ms} ms)')
+ check(client.read(f'/pane/{edited}/body') == b'apple\npear\n', 'and the slow Edit applied')
+ ran = run(f"echo 'Edit $ < cat {mount}/pane/{edited}/body' > {mount}/pane/{edited}/ctl\n".encode())
+ check(ran == '0' and client.read(f'/pane/{edited}/body') == b'apple\npear\napple\npear\n',
+ f'an Edit whose command reads the session through its mount ({ran!r})')
+ # A filter gets what a pane's command gets: the session's mount,
+ # socket and pid.
+ env_pane = int(client.read('/pane/new'))
+ ran = run(f"echo 'Edit $ < printf \"%s|%s|%s\" \"$PARDES_MOUNT\" \"$PARDES_9P\" \"$PARDES_PID\"' > {mount}/pane/{env_pane}/ctl\n".encode())
+ got_env = client.read(f'/pane/{env_pane}/body').decode().split('|')
+ check(ran == '0' and len(got_env) == 3 and got_env[0] == mount and got_env[1] == sock and got_env[2].isdigit(),
+ f'an Edit filter has $PARDES_MOUNT, $PARDES_9P and $PARDES_PID ({got_env!r})')
+ # An interrupted Edit write is flushed: its command is stopped and
+ # nothing changes, and the next Edit runs.
+ flushed = Path(f'/tmp/{name}.flushed')
+ ran = run((f"timeout -s INT 1 sh -c \"echo 'Edit , | sleep 4; tr a-z A-Z' > {mount}/pane/{edited}/ctl\"; "
+ f"echo $? > {flushed}; sleep 1; echo 'Edit 1 | tr p P' > {mount}/pane/{edited}/ctl; echo $? >> {flushed}\n").encode())
+ codes = flushed.read_text().split() if flushed.exists() else []
+ flushed.unlink(missing_ok=True)
+ time.sleep(4.5)
+ check(codes == ['124', '0'] and client.read(f'/pane/{edited}/body') == b'aPPle\npear\napple\npear\n',
+ f'an interrupted Edit changes nothing, and the next runs ({codes!r}, {client.read(f"/pane/{edited}/body")!r})')
client.write('/exec', b'Msg still here\n')
- text = ''.join(cell[0] for cell in client.screen()['cells'])
+ # The terminal is read as it is waited on: a frame the editor
+ # cannot write out (its pty full) is a frame /screen does not have.
+ deadline = time.monotonic() + 8
+ while 'still here' not in (text := ''.join(cell[0] for cell in client.screen()['cells'])) and time.monotonic() < deadline:
+ drain()
+ time.sleep(.1)
check('still here' in text, 'the editor answers after all of that')
+ # A filter that writes its own pane's ctl through the mount. Both
+ # sides with `<>` run at once; a `>` or `>>` open on either side
+ # waits in the kernel for the other write, and the command is
+ # stopped at its 10 s limit: the Edit's write is answered then,
+ # never held by a command it cannot kill.
+ loop_pane = env_pane # the screen has no room for another
+ client.write(f'/pane/{loop_pane}/body', b'x\n', truncate=True)
+ inner = Path(f'/tmp/{name}.inner')
+ timed = Path(f'/tmp/{name}.timed')
+ for outer_open, inner_open, quick in [('3<>', '1<>', True), ('>', '>', False), ('>>', '>', False),
+ ('3<>', '>>', False), ('>', '>>', False)]:
+ # `clean`, which says nothing: a stopped filter's write still
+ # lands once the Edit's is answered.
+ inner.write_text(f'echo clean {inner_open} "$PARDES_MOUNT/pane/$winid/ctl"\n')
+ edit = f'Edit 1 > sh {inner}'
+ write = (f"exec 3<>{mount}/pane/{loop_pane}/ctl; echo '{edit}' >&3; rc=$?; exec 3>&-"
+ if outer_open == '3<>' else f"echo '{edit}' {outer_open} {mount}/pane/{loop_pane}/ctl; rc=$?")
+ ran = run((f"s=$(date +%s%N); {write}; e=$(date +%s%N); "
+ f"echo $rc $(( (e - s) / 1000000 )) > {timed}\n").encode())
+ rc_ms = timed.read_text().split() if timed.exists() else ['?', '-1']
+ timed.unlink(missing_ok=True)
+ ms = int(rc_ms[1])
+ ok = ms >= 0 and (ms < 2000 if quick else ms < 13000)
+ check(ran == '0' and ok, f'outer {outer_open}, filter {inner_open} on its own ctl: answered in {ms} ms (rc {rc_ms[0]})')
+ inner.unlink(missing_ok=True)
+ client.write('/exec', b'Msg after the stopped filters\n')
+ check('msg ' in client.read('/log').decode().split('after the stopped filters')[0][-40:],
+ 'and the editor answers after them')
finally:
os.kill(pid, signal.SIGTERM)
time.sleep(.2)