diff options
Diffstat (limited to 'test/selfmount.py')
| -rw-r--r-- | test/selfmount.py | 115 |
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) |
