From 4d0df004e93c19e2a10076d8a7b75ad6b3081051 Mon Sep 17 00:00:00 2001 From: Mario Zechner Date: Wed, 25 Jun 2025 13:52:40 +0200 Subject: [PATCH] Minimal pty test --- web/fwd-test.ts | 202 ++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 202 insertions(+) create mode 100755 web/fwd-test.ts diff --git a/web/fwd-test.ts b/web/fwd-test.ts new file mode 100755 index 00000000..7468f7b7 --- /dev/null +++ b/web/fwd-test.ts @@ -0,0 +1,202 @@ +#!/usr/bin/env pnpm exec tsx --no-deprecation + +/** + * Minimal test script for node-pty + * Tests PTY spawning, terminal raw mode, stdin/stdout forwarding + */ + +import * as pty from '@homebridge/node-pty-prebuilt-multiarch'; +import { which } from '@homebridge/node-pty-prebuilt-multiarch/lib/utils'; +import * as fs from 'fs'; +import * as path from 'path'; +import * as os from 'os'; + +// Terminal state restoration +let originalStdinRawMode = false; +let ptyProcess: pty.IPty | null = null; + +/** + * Clean up and restore terminal state + */ +function cleanup() { + // Restore terminal mode + if (process.stdin.isTTY && originalStdinRawMode) { + try { + process.stdin.setRawMode(false); + } catch (e) { + // Ignore errors + } + } + + // Kill PTY process + if (ptyProcess) { + try { + ptyProcess.kill(); + } catch (e) { + // Process might already be dead + } + ptyProcess = null; + } + + // Clear line and show cursor + process.stdout.write('\r\n\x1b[?25h'); +} + +/** + * Resolve command using shell-like logic + */ +function resolveCommand(args: string[]): { command: string; args: string[] } { + if (args.length === 0) { + throw new Error('No command specified'); + } + + const [cmd, ...cmdArgs] = args; + + // Try to find the command in PATH + try { + const resolved = which(cmd); + if (resolved) { + return { command: resolved, args: cmdArgs }; + } + } catch (e) { + // Command not found in PATH + } + + // Check if it's a relative path that exists + if (cmd.includes('/') || cmd.includes('\\')) { + const fullPath = path.resolve(cmd); + if (fs.existsSync(fullPath)) { + return { command: fullPath, args: cmdArgs }; + } + } + + // Check common shell aliases + const shellAliases: Record = { + 'll': ['ls', '-la'], + 'la': ['ls', '-la'], + 'l': ['ls', '-l'], + }; + + if (shellAliases[cmd]) { + const [aliasCmd, ...aliasArgs] = shellAliases[cmd]; + return resolveCommand([aliasCmd, ...aliasArgs, ...cmdArgs]); + } + + // Fallback to original command and let PTY handle it + return { command: cmd, args: cmdArgs }; +} + +/** + * Main function + */ +async function main() { + // Parse command line arguments + const args = process.argv.slice(2); + + if (args.length === 0 || args[0] === '--help' || args[0] === '-h') { + console.log('Usage: ./fwd-test.ts [args...]'); + console.log(''); + console.log('Examples:'); + console.log(' ./fwd-test.ts bash'); + console.log(' ./fwd-test.ts ls -la'); + console.log(' ./fwd-test.ts python3 -i'); + process.exit(0); + } + + try { + // Resolve command + const { command, args: resolvedArgs } = resolveCommand(args); + console.log(`Spawning: ${command} ${resolvedArgs.join(' ')}`); + console.log(`Working directory: ${process.cwd()}`); + console.log(''); + + // Get terminal size + const cols = process.stdout.columns || 80; + const rows = process.stdout.rows || 24; + + // Spawn PTY + ptyProcess = pty.spawn(command, resolvedArgs, { + name: 'xterm-256color', + cols, + rows, + cwd: process.cwd(), + env: { + ...process.env, + TERM: 'xterm-256color', + }, + }); + + console.log(`PTY spawned with PID: ${ptyProcess.pid}`); + console.log('Press Ctrl+C to exit\n'); + + // Forward PTY output to stdout + ptyProcess.onData((data: string) => { + process.stdout.write(data); + }); + + // Handle PTY exit + ptyProcess.onExit(({ exitCode, signal }) => { + console.log(`\nProcess exited with code ${exitCode}${signal ? ` (signal: ${signal})` : ''}`); + cleanup(); + process.exit(exitCode || 0); + }); + + // Set terminal to raw mode + if (process.stdin.isTTY) { + originalStdinRawMode = process.stdin.isRaw; + process.stdin.setRawMode(true); + } + process.stdin.resume(); + + // Forward stdin to PTY + process.stdin.on('data', (data: Buffer) => { + if (ptyProcess) { + ptyProcess.write(data.toString()); + } + }); + + // Handle terminal resize + process.stdout.on('resize', () => { + if (ptyProcess) { + const newCols = process.stdout.columns || 80; + const newRows = process.stdout.rows || 24; + ptyProcess.resize(newCols, newRows); + } + }); + + // Handle signals for cleanup + const signals: NodeJS.Signals[] = ['SIGINT', 'SIGTERM', 'SIGQUIT']; + signals.forEach(signal => { + process.on(signal, () => { + console.log(`\nReceived ${signal}, cleaning up...`); + cleanup(); + process.exit(1); + }); + }); + + // Handle uncaught errors + process.on('uncaughtException', (error) => { + console.error('\nUncaught exception:', error); + cleanup(); + process.exit(1); + }); + + process.on('unhandledRejection', (reason) => { + console.error('\nUnhandled rejection:', reason); + cleanup(); + process.exit(1); + }); + + } catch (error) { + console.error('Error:', error); + cleanup(); + process.exit(1); + } +} + +// Run main function +main().catch((error) => { + console.error('Fatal error:', error); + cleanup(); + process.exit(1); +}); \ No newline at end of file