update docs

This commit is contained in:
2026-09-22 13:23:34 +08:00
parent 99bc3d15c5
commit 8a4788fca8
126 changed files with 7198 additions and 2425 deletions
+127 -17
View File
@@ -3,6 +3,8 @@
import argparse
import io
import json
import hashlib
import lzma
import os
import pty
import selectors
@@ -63,6 +65,73 @@ def clean(number):
assert f'/run/fds/software/{number:02}/' not in text
assert '/run/fds/apps/demo.workstation' not in text
def interactive_program_checks():
master, slave = pty.openpty()
original = termios.tcgetattr(slave)
console = subprocess.Popen([cli, '--session', str(session), 'console'], stdin=slave, stdout=slave, stderr=slave)
selector = selectors.DefaultSelector(); selector.register(master, selectors.EVENT_READ)
pending = bytearray()
def until(marker):
output = pending; deadline = time.monotonic() + 30
while marker not in output:
assert time.monotonic() < deadline, output
for _, _ in selector.select(max(0, deadline-time.monotonic())):
chunk = os.read(master, 8192); assert chunk
output.extend(chunk)
end = output.index(marker) + len(marker)
result = bytes(output[:end]); del output[:end]
log.write(repr(result) + '\n'); log.flush()
return result
try:
until(b'FDS> ')
os.write(master, b"stty -g >/tmp/program-tty-before; b01:demo.report:shell -c 'printf \"READY:%s\\n\" tty; read -r line; printf \"REPLY:%s\\n\" \"$line\"'\n")
until(b'READY:tty\r\n')
os.write(master, b'literal interactive input\n')
until(b'REPLY:literal interactive input\r\n')
until(b'FDS> ')
os.write(master, b"b01:demo.report:shell -c 'printf \"SIGNAL:%s\\n\" ready; exec tail -f /dev/null'; printf 'RETURN:%s\\n' \"$?\"\n")
until(b'SIGNAL:ready\r\n')
os.write(master, b'\x03')
until(b'RETURN:130\r\n')
until(b'FDS> ')
os.write(master, b"b01:demo.report:shell -c 'printf \"STOP:%s\\n\" ready; exec tail -f /dev/null'\n")
until(b'STOP:ready\r\n')
os.write(master, b'\x1a')
until(b'Stopped')
until(b'FDS> ')
os.write(master, b'fg\n')
until(b"exec tail -f /dev/null'\r\n")
os.write(master, b'\x03')
until(b'FDS> ')
os.write(master, b"test \"$(stty -g)\" = \"$(cat /tmp/program-tty-before)\" && printf 'RESTORE:%s\\n' passed\n")
until(b'RESTORE:passed\r\n')
os.write(master, b'\x1d')
assert console.wait(timeout=10) == 0
assert termios.tcgetattr(slave) == original
finally:
if console.poll() is None: console.kill(); console.wait()
selector.close(); os.close(master); os.close(slave)
# Compatibility fixture only: the public creator never emits archive payloads.
legacy_metadata=work/'legacy-metadata'; (legacy_metadata/'FDS').mkdir(parents=True)
legacy_payload=work/'legacy-payload'; (legacy_payload/'bundles').mkdir(parents=True)
legacy_program=b'#!/bin/sh\nprintf "Legacy reader: %s\\n" "$1"\n'
legacy_tar=io.BytesIO()
with tarfile.open(fileobj=legacy_tar,mode='w',format=tarfile.USTAR_FORMAT) as archive:
directory=tarfile.TarInfo('bin');directory.type=tarfile.DIRTYPE;directory.mode=0o755;archive.addfile(directory)
program=tarfile.TarInfo('bin/legacy');program.mode=0o755;program.size=len(legacy_program);archive.addfile(program,io.BytesIO(legacy_program))
legacy_bytes=lzma.compress(legacy_tar.getvalue(),format=lzma.FORMAT_XZ)
(legacy_payload/'bundles/legacy.reader.tar.xz').write_bytes(legacy_bytes)
(legacy_metadata/'FDS/CARTRIDGE.TOML').write_text('format=1\n[cartridge]\nid="legacy.fixture"\nname="Legacy reader fixture"\nclass="program"\nversion="1"\n[media]\nwritable=false\n')
(legacy_metadata/'FDS/SOFTWARE.TOML').write_text(f'format=1\n[[software]]\nid="legacy.reader"\nname="Legacy reader"\nversion="1"\narchitecture="any"\npartition=2\narchive_bytes={len(legacy_bytes)}\nunpacked_bytes={len(legacy_program)}\nentries=2\nsha256="{hashlib.sha256(legacy_bytes).hexdigest()}"\n[software.commands]\nlegacy="bin/legacy"\n')
legacy_parts=[]
for name,tree in [('FDS_METADATA',legacy_metadata),('FDS_PAYLOAD02',legacy_payload)]:
filesystem=work/(name+'.erofs')
subprocess.run([str(project/'tools/in-image-tools'),'mkfs.erofs','--quiet','-T','0',str(filesystem),str(tree)],check=True,stdout=log,stderr=log)
legacy_parts.append((name,LINUX_FILESYSTEM,filesystem))
legacy=work/'legacy.img';gpt(legacy,legacy_parts)
subprocess.run([str(project/'out/workstation/fds-cartridge'),'--image-tool-runner',str(project/'tools/in-image-tools'),'inspect',str(legacy)],check=True,stdout=log,stderr=log)
# A workstation-created disposable DATA fixture, never a host block device.
data_root = work / 'data-root'
(data_root / 'FDS').mkdir(parents=True)
@@ -118,32 +187,59 @@ try:
if number in [1, 6, 12]:
guest('fds', 'run', number, '--', 'demo.hello:hello')
guest('fds', 'run', number, '--', 'demo.report:report')
guest('touch', f'/run/fds/software/{number:02}/cache-demo.hello/unexpected', ok=False)
guest('touch', f'/run/fds/software/{number:02}/payload02/programs/demo.hello/unexpected', ok=False)
guest('fds', 'run', number, '--', '../escape', ok=False)
guest('fds', 'run', number, '--', 'demo.hello:missing', ok=False)
assert guest('hello', 'literal $(id); and spaces').strip() == 'Hello from an FDS AArch64 software cartridge: literal $(id); and spaces'
assert guest(f'b{number:02}:demo.report:shell', '-c', 'id -u').strip() == '1000'
assert guest('sh', '-c', f'printf "pipe input" | b{number:02}:demo.report:shell -c "cat"') == 'pipe input'
assert guest('sh', '-c', f'b{number:02}:demo.report:shell -c "exit 37"; printf "%s" "$?"') == '37'
assert guest('sh', '-c', f'cd /tmp; b{number:02}:demo.report:shell -c "pwd"').strip() == '/tmp'
assert '/cache-' not in mounts()
assert state['commands']
if number == 1:
interactive_program_checks()
if number == 6:
guest('fds', 'run', number, '--', 'demo.report:report', 'hold')
assert query('bay', number)['bays'][0]['consumers'] > 0
guest('sh', '-c', 'report hold >/tmp/direct-hold.log 2>&1 & echo $! >/tmp/direct-launcher')
deadline = time.monotonic() + 20
while query('bay', number)['bays'][0]['consumers'] < 2:
assert time.monotonic() < deadline
assert query('bay', number)['bays'][0]['consumers'] >= 2
invoke('unplug', number)
else:
invoke('eject', number)
state = wait_bay(number, 'empty')
assert state['consumers'] == 0
clean(number)
if number == 6:
deadline = time.monotonic() + 20
while guest('sh', '-c', 'test ! -e /proc/$(cat /tmp/direct-launcher); printf "%s" "$?"').strip() != '0':
assert time.monotonic() < deadline
assert not guest('sh', '-c', 'command -v hello || true').strip()
# Two different cartridges export the same names. Lowest bay wins, and
# fully qualified commands remain available before and after an eject.
invoke('insert', 8, software); wait_bay(8, 'mounted_read_only')
invoke('insert', 2, builder / 'collision.img'); wait_bay(2, 'mounted_read_only')
assert '/02/' in guest('where', 'FDS_APP')
assert '/08/' in guest('b08:demo.report:where', 'FDS_APP')
invoke('eject', 2); wait_bay(2, 'empty')
assert '/08/' in guest('where', 'FDS_APP')
invoke('eject', 8); wait_bay(8, 'empty')
remaining = json.loads(invoke('status'))
assert remaining['state']['cartridges'] == {}
assert len(remaining['block_nodes']) == 2, remaining['block_nodes']
output = guest('cat', '/run/log/cartridged/current')
assert 'FDS cartridge AArch64 hello' in output
assert 'FDS cartridge script report' in output
assert 'Hello from an FDS AArch64 software cartridge' in output
assert 'FDS cartridge system report' in output
assert 'uid=1000(fds)' in output
(work / 'program-output.log').write_text(output)
invoke('insert', 3, builder / 'archive-corrupt.img')
wait_bay(3, 'mounted_read_only')
failure = guest('fds', 'run', 3, '--', 'demo.hello:hello', ok=False)
assert 'digest' in failure.lower() or 'sha' in failure.lower(), failure
assert '/run/fds/software/03/cache-' not in mounts()
invoke('eject', 3)
invoke('insert', 3, builder / 'program-corrupt.img')
failed = wait_bay(3, 'error')
assert 'digest' in failed['detail'].lower() or 'hash' in failed['detail'].lower(), failed
assert '/run/fds/software/03/' not in mounts()
invoke('unplug', 3)
wait_bay(3, 'empty')
invoke('insert', 4, builder / 'catalogue-mapping.img')
wait_bay(4, 'error')
@@ -162,7 +258,13 @@ try:
assert digest(data) == original_data
assert overlay.is_file()
assert len(json.loads(invoke('status'))['block_nodes']) == 2
# Keep a verified software cache mounted across native shutdown.
invoke('insert', 7, legacy); wait_bay(7, 'mounted_read_only')
assert guest('legacy', 'compatibility').strip() == 'Legacy reader: compatibility'
assert '/run/fds/software/07/cache-legacy.reader' in mounts()
guest('fds', 'run', 7, '--', 'legacy.reader:legacy', 'background')
invoke('eject', 7); wait_bay(7, 'empty'); clean(7)
assert not guest('sh', '-c', 'command -v legacy || true').strip()
# Keep a directly mounted software tree active across native shutdown.
invoke('insert', 12, software)
wait_bay(12, 'mounted_read_only')
guest('fds', 'run', 12, '--', 'demo.report:report', 'hold')
@@ -193,15 +295,22 @@ try:
assert guest('id', '-u').strip() == '0'
invoke('insert', 1, software)
wait_bay(1, 'mounted_read_only')
guest('fds', 'run', 1, '--', 'demo.report:report', 'hold')
assert '/run/fds/software/01/cache-demo.report' in mounts()
guest('sh', '-c', 'cd /home/fds; /run/fds/bin/report hold >/tmp/restart-direct.log 2>&1 &')
deadline = time.monotonic() + 20
while query('bay', 1)['bays'][0]['consumers'] == 0:
assert time.monotonic() < deadline
assert '/run/fds/software/01/payload03' in mounts()
assert '/cache-' not in mounts()
guest('s6-rc', '-l', '/run/s6-rc', '-d', 'change', 'cartridged')
guest('s6-rc', '-l', '/run/s6-rc', '-u', 'change', 'cartridged')
wait_bay(1, 'mounted_read_only')
assert query('bay', 1)['bays'][0]['consumers'] == 0
assert '/run/fds/software/01/cache-' not in mounts()
guest('fds', 'run', 1, '--', 'demo.hello:hello')
# Extra payload aliases must prevent SAFE; cache is genuinely read-only.
assert 'Hello from' in guest('sh', '-c', 'cd /home/fds; /run/fds/bin/hello')
guest('mkdir', '-m', '700', '/tmp/private-cwd')
assert 'Permission denied' in guest('sh', '-c', 'cd /tmp/private-cwd; /run/fds/bin/hello', ok=False)
# Extra payload aliases must prevent SAFE; program trees are read-only.
guest('mkdir', '/run/extra-payload')
guest('mount', '--bind', '/run/fds/software/01/payload02', '/run/extra-payload')
invoke('eject', 1, ok=False)
@@ -216,10 +325,11 @@ finally:
assert digest(software) == original_software and digest(data) == original_data
record = dict(status='passed', ordinary_guest_uid=1000, public_emulator_cli=True,
all_twelve_usb_bays=True, interactive_console_detach_and_terminal_restore=True, both_payload_partitions_executed=True,
shared_partition_executed=True, readonly_cache=True,
shared_partition_executed=True, readonly_program_trees_without_extraction=True, legacy_archive_host_and_guest_reader=True,
direct_path_arguments_pipes_exit_status_cwd=True, interactive_program_io_signals_and_restore=True, command_collision_fallback=True,
safe_eject=True, forced_removal_stops_consumer=True,
corrupt_archive_and_catalogue_rejected=True, data_overlay_preserves_source=True,
restart_cleans_cache_and_consumers=True, extra_mount_blocks_safe=True,
corrupt_program_and_catalogue_rejected=True, data_overlay_preserves_source=True,
restart_cleans_mounts_commands_and_consumers=True, extra_mount_blocks_safe=True,
native_shutdown_with_active_software=True, physical_pi='not tested')
(work / 'acceptance.json').write_text(json.dumps(record, indent=2) + '\n')
(project / 'out/workstation-emulator-current.txt').write_text(str(work) + '\n')