1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
|
#!/usr/bin/env python3
"""A session that opens, reads and saves its own 9P tree through a mount.
The editor runs under `9ns --mntgen`, so /mnt/9p/pardes/<name> inside its
namespace is its own tree, and is driven over its socket from outside. This
used to hang the editor outright: the realpath, the stat and the read went
out through the mount and came back as 9P requests only the editor's loop
could answer, while that loop was blocked making them. Now the connection's
task answers them while the editor waits (pardes.turn).
Needs 9ns (cloud9) on PATH or in ~/.local/bin, and a runtime directory; it
skips, loudly, without them.
"""
import fcntl
import os
from pathlib import Path
import pty
import shutil
import signal
import struct
import sys
import termios
import time
sys.path.insert(0, str(Path(__file__).resolve().parent))
from ninep import Client
def run(binary):
ns = shutil.which('9ns') or str(Path.home() / '.local/bin/9ns')
runtime = os.environ.get('XDG_RUNTIME_DIR')
if not os.path.exists(ns) or not runtime:
print('selfmount: skipped (needs 9ns and XDG_RUNTIME_DIR)')
return 0
name = 'selfmount%d' % os.getpid()
sock = f'{runtime}/pardes-9p-{name}.sock'
fixture = Path(f'/tmp/{name}.txt')
fixture.write_bytes(b'initial\n')
env = {k: v for k, v in os.environ.items() if not k.startswith('PARDES_')}
env.update(PARDES_NOTIME='1', TERM='xterm-256color', PARDES_DUMP=f'/tmp/{name}.dump.zon')
for language in ['RS', 'C', 'GO', 'TS', 'PY']:
env['PARDES_LSP_' + language] = ''
pid, master = pty.fork()
if pid == 0:
os.execve(ns, ['9ns', '--mntgen', '--', binary, '--tty', '--9p=' + name, str(fixture)], env)
fcntl.ioctl(master, termios.TIOCSWINSZ, struct.pack('HHHH', 30, 100, 0, 0))
os.set_blocking(master, False)
tail = bytearray()
def drain():
try:
while True:
chunk = os.read(master, 65536)
if not chunk:
return
tail.extend(chunk[-2000:])
except OSError:
pass
failures = []
def check(condition, what):
print(('ok ' if condition else 'FAIL ') + what)
if not condition:
failures.append(what)
try:
deadline = time.monotonic() + 15
while not os.path.exists(sock):
drain()
if time.monotonic() > deadline:
raise TimeoutError('socket never appeared: ' + tail.decode(errors='replace')[-400:])
time.sleep(.02)
time.sleep(.3)
mount = f'/mnt/9p/pardes/{name}'
with Client(sock) as client:
index = client.read('/index').decode()
rows = [row.rsplit(maxsplit=1)[0].split(maxsplit=3) for row in index.splitlines()]
first = rows[0][0]
# A Look at the tree's own index: realpath, stat and read all go
# out through 9ns and back in, and the write returns when the
# pane is open.
started = time.monotonic()
client.write('/look', (mount + '/index\n').encode())
took = time.monotonic() - started
check(took < 5, f'look at the mounted index returned ({took:.2f}s)')
rows = [row.rsplit(maxsplit=1)[0].split(maxsplit=3) for row in client.read('/index').decode().splitlines()]
opened = [row for row in rows if len(row) == 4 and row[3] == mount + '/index']
check(len(opened) == 1, 'a pane opened on the mounted index')
if opened:
body = client.read(f'/pane/{opened[0][0]}/body').decode()
check(body.startswith(index.splitlines()[0]), 'its body is the index as served')
# A Look at pane 1's body through the mount, an edit of the copy,
# and a Save: the save writes back through the mount into pane 1,
# and the exec returns once it has.
client.write('/look', f'{mount}/pane/{first}/body\n'.encode())
rows = [row.rsplit(maxsplit=1)[0].split(maxsplit=3) for row in client.read('/index').decode().splitlines()]
mirror = [row for row in rows if len(row) == 4 and row[3] == f'{mount}/pane/{first}/body']
check(len(mirror) == 1, 'a pane opened on the mounted body')
if mirror:
pane = mirror[0][0]
client.write(f'/pane/{pane}/addr', b'#0,#0')
client.write(f'/pane/{pane}/data', b'HELLO ')
client.write(f'/pane/{pane}/exec', b'Save\n')
check(client.read(f'/pane/{first}/body') == b'HELLO initial\n', f'Save wrote back through the mount into pane {first}')
text = ''.join(cell[0] for cell in client.screen()['cells'])
check('saved ' + mount in text, 'the editor reported the save')
client.write('/exec', b'Msg still here\n')
text = ''.join(cell[0] for cell in client.screen()['cells'])
check('still here' in text, 'the editor answers after all of that')
finally:
os.kill(pid, signal.SIGTERM)
time.sleep(.2)
try:
os.kill(pid, signal.SIGKILL)
except ProcessLookupError:
pass
os.waitpid(pid, 0)
for path in (fixture, Path(f'/tmp/{name}.dump.zon')):
path.unlink(missing_ok=True)
print('selfmount: ' + ('ok' if not failures else f'{len(failures)} failed'))
return 0 if not failures else 1
if __name__ == '__main__':
sys.exit(run(str(Path(sys.argv[1]).resolve())))
|