Update
This commit is contained in:
@@ -0,0 +1,154 @@
|
||||
#!/usr/bin/env node
|
||||
/**
|
||||
* Standalone TTY repro for fish-style readline (lone `>`, missing `[user@host:path]`).
|
||||
*
|
||||
* Run in a real terminal (not piped):
|
||||
* cd packages/bare-os-booter && node scripts/fish-tty-repro.js
|
||||
* # or: npm run repro:fish-tty -w bare-os-booter
|
||||
*
|
||||
* Automated PTY (drains stdout so Node does not block; checks for green prompt):
|
||||
* python3 scripts/run-fish-repro-pty.py
|
||||
* # or: npm run repro:fish-tty:pty -w bare-os-booter
|
||||
*
|
||||
* Env:
|
||||
* BARE_OS_REPRO_SKIP_STDERR=1 — omit stderr preamble (isolates stdout/stderr ordering).
|
||||
* BARE_OS_REPRO_SKIP_PREAMBLE=1 — skip fake kernel lines; only fish prompt.
|
||||
* BARE_OS_FISH_RESYNC_LINE=0 — disable leading newline before each prompt (default is on).
|
||||
* DEBUG=1 — verbose fish/TTY logs on the same terminal as the shell (debug-repl.js).
|
||||
*
|
||||
* Type a few commands; use `exit` or Ctrl+D to quit. Compare first prompt vs after ^C.
|
||||
*/
|
||||
|
||||
import b4a from 'b4a'
|
||||
import { bindReplDebugStream, unbindReplDebugStream } from '../lib/debug-repl.js'
|
||||
import {
|
||||
createFishReadLine,
|
||||
disableFishRawMode,
|
||||
releaseFishStdin
|
||||
} from '../lib/fish-readline.js'
|
||||
|
||||
function mockCtx() {
|
||||
return {
|
||||
vfs: {
|
||||
getcwd: () => '/home/user',
|
||||
resolveLogical: (p) => (p === '' ? '/' : p),
|
||||
async readdir() {
|
||||
return []
|
||||
}
|
||||
},
|
||||
drive: {
|
||||
readdir() {
|
||||
async function* empty() {
|
||||
/* no /bin entries */
|
||||
}
|
||||
return empty()
|
||||
}
|
||||
},
|
||||
personalDrive: {
|
||||
async get() {
|
||||
return null
|
||||
},
|
||||
async put() {}
|
||||
},
|
||||
b4a,
|
||||
env: {
|
||||
HOME: '/home/user',
|
||||
USER: 'user',
|
||||
HOSTNAME: 'bare-os',
|
||||
PATH: '/bin',
|
||||
PWD: '/home/user'
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** Same idea as executeKernel writeScreen (TTY clear hook). */
|
||||
function makeWriteScreen(stdout) {
|
||||
return (chunk) => {
|
||||
const s = typeof chunk === 'string' ? chunk : String(chunk)
|
||||
stdout.write(s)
|
||||
}
|
||||
}
|
||||
|
||||
function kernelPreambleToStdout(stdout) {
|
||||
const lines = [
|
||||
'NAME="BareOS"',
|
||||
'VERSION="0.1.0"',
|
||||
'VARIANT="hyperdrive-only"',
|
||||
'',
|
||||
'Bare operating system — POSIX-ish shell: cd, export, exit, quit | try: help, ls /bin, pwd'
|
||||
]
|
||||
for (const line of lines) stdout.write(line + '\n')
|
||||
}
|
||||
|
||||
async function main() {
|
||||
const stdin = process.stdin
|
||||
const stdout = process.stdout
|
||||
|
||||
if (!stdin.isTTY || typeof stdin.setRawMode !== 'function') {
|
||||
process.stderr.write(
|
||||
'fish-tty-repro: need a TTY with setRawMode (run in a terminal, not a pipe).\n'
|
||||
)
|
||||
process.exit(1)
|
||||
}
|
||||
|
||||
const skipStderr = process.env.BARE_OS_REPRO_SKIP_STDERR === '1'
|
||||
const skipPreamble = process.env.BARE_OS_REPRO_SKIP_PREAMBLE === '1'
|
||||
|
||||
if (!skipStderr) {
|
||||
process.stderr.write(
|
||||
'\x1b[0m[bare-os-booter] Fish-style TTY line editor active\n'
|
||||
)
|
||||
}
|
||||
|
||||
const ctx = mockCtx()
|
||||
const writeScreen = makeWriteScreen(stdout)
|
||||
|
||||
if (process.env.DEBUG === '1') bindReplDebugStream(stdout)
|
||||
|
||||
const fishRead = await createFishReadLine(ctx, {
|
||||
stdin,
|
||||
stdout,
|
||||
writeScreen
|
||||
})
|
||||
|
||||
if (!fishRead) {
|
||||
process.stderr.write('createFishReadLine returned null.\n')
|
||||
process.exit(1)
|
||||
}
|
||||
|
||||
if (!skipPreamble) {
|
||||
kernelPreambleToStdout(stdout)
|
||||
}
|
||||
|
||||
const readLine = (prompt) => fishRead(prompt)
|
||||
|
||||
process.stderr.write(
|
||||
'\n--- repro ready: first prompts should show full [user@bare-os:~] > if healthy ---\n'
|
||||
)
|
||||
|
||||
try {
|
||||
while (true) {
|
||||
const line = await readLine('repro> ')
|
||||
if (line == null) {
|
||||
stdout.write('\n')
|
||||
break
|
||||
}
|
||||
const t = line.trim()
|
||||
if (t === 'exit' || t === 'quit') break
|
||||
if (t === 'cls' || t === 'clear') {
|
||||
writeScreen('\x1b[H\x1b[2J\x1b[3J')
|
||||
continue
|
||||
}
|
||||
if (t) stdout.write(`(cmd) ${t}\n`)
|
||||
}
|
||||
} finally {
|
||||
disableFishRawMode(stdin)
|
||||
releaseFishStdin(stdin)
|
||||
unbindReplDebugStream()
|
||||
}
|
||||
}
|
||||
|
||||
main().catch((err) => {
|
||||
process.stderr.write(String(err?.stack || err) + '\n')
|
||||
process.exit(1)
|
||||
})
|
||||
@@ -0,0 +1,92 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Run fish-tty-repro.js under a pseudo-TTY and drain output so Node never blocks on write.
|
||||
|
||||
Usage (from anywhere):
|
||||
python3 packages/bare-os-booter/scripts/run-fish-repro-pty.py
|
||||
|
||||
BARE_OS_REPRO_SKIP_STDERR=1 python3 packages/bare-os-booter/scripts/run-fish-repro-pty.py
|
||||
|
||||
BARE_OS_REPRO_PTY_ASSERT=1 ... # exit 2 if green prompt not detected (CI guard)
|
||||
|
||||
Requires: Unix (pty). Not for Windows CI.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
import pty
|
||||
import select
|
||||
import sys
|
||||
import threading
|
||||
import time
|
||||
|
||||
HERE = os.path.dirname(os.path.abspath(__file__))
|
||||
BOOTER = os.path.dirname(HERE)
|
||||
|
||||
|
||||
def main() -> int:
|
||||
chunks: list[bytes] = []
|
||||
|
||||
def reader(master_fd: int, done: threading.Event) -> None:
|
||||
while not done.is_set():
|
||||
try:
|
||||
r, _, _ = select.select([master_fd], [], [], 0.2)
|
||||
except (OSError, ValueError):
|
||||
break
|
||||
if done.is_set():
|
||||
break
|
||||
if not r:
|
||||
continue
|
||||
try:
|
||||
data = os.read(master_fd, 65536)
|
||||
except OSError:
|
||||
break
|
||||
if not data:
|
||||
break
|
||||
chunks.append(data)
|
||||
|
||||
pid, master_fd = pty.fork()
|
||||
if pid == 0:
|
||||
os.chdir(BOOTER)
|
||||
os.execlp("node", "node", os.path.join(HERE, "fish-tty-repro.js"))
|
||||
return 0 # unreachable
|
||||
|
||||
done = threading.Event()
|
||||
t = threading.Thread(target=reader, args=(master_fd, done))
|
||||
t.start()
|
||||
time.sleep(0.6)
|
||||
os.write(master_fd, b"hello\n")
|
||||
time.sleep(0.35)
|
||||
os.write(master_fd, b"exit\n")
|
||||
# Wait for Node to exit before closing the PTY master (avoids exit code 1 / hang).
|
||||
_, status = os.waitpid(pid, 0)
|
||||
|
||||
done.set()
|
||||
t.join(timeout=5.0)
|
||||
try:
|
||||
os.close(master_fd)
|
||||
except OSError:
|
||||
pass
|
||||
raw = b"".join(chunks)
|
||||
try:
|
||||
sys.stdout.buffer.write(raw)
|
||||
except BrokenPipeError:
|
||||
pass
|
||||
|
||||
text = raw.decode("utf-8", errors="replace")
|
||||
ok_green = "\x1b[32m[user@bare-os" in text or "[user@bare-os" in text
|
||||
|
||||
sys.stderr.write(
|
||||
f"\n--- pty harness: exit status {status}, bytes {len(raw)}, "
|
||||
f"saw_full_prompt_hint={ok_green} ---\n"
|
||||
)
|
||||
if os.environ.get("BARE_OS_REPRO_PTY_ASSERT") == "1" and not ok_green:
|
||||
sys.stderr.write("BARE_OS_REPRO_PTY_ASSERT: expected green [user@bare-os] prompt\n")
|
||||
return 2
|
||||
if os.WIFEXITED(status):
|
||||
return os.WEXITSTATUS(status)
|
||||
return 1
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
Reference in New Issue
Block a user