char-slop/berry
multipurpose self-modifiable agentic harness
git clone https://git.t4t.associates/char-slop/berry
0575637
main
1// Programs on pseudo-terminals of this machine, through libc, since Deno has no terminals of its own. Linux only. 2 3import type { Context } from "@earendil-works/chord" ; 4import type { Pty , PtyOptions } from "./environment.ts" ; 5import { windowsPty } from "./pty-windows.ts" ; 6 7const O_RDWR = 0o2 ; 8const O_NOCTTY = 0o400 ; 9const O_NONBLOCK = 0o4000 ; 10// or every program started later would hold this terminal open too 11const O_CLOEXEC = 0o2000000 ; 12const TIOCSWINSZ = 0x5414n ; 13const POSIX_SPAWN_SETSID = 0x80 ; 14const ENOENT = 2 ; 15const EINTR = 4 ; 16const EIO = 5 ; 17const EAGAIN = 11 ; 18const POLLIN = 0x1 ; 19const POLLOUT = 0x4 ; 20/** How long a terminal stays open once its program has exited, for what it wrote last. */ 21const GRACE_MS = 250 ; 22/** Room for glibc's and musl's posix_spawnattr_t and posix_spawn_file_actions_t, which are smaller. */ 23const OPAQUE = 1024 ; 24 25let libc :ReturnType < typeof openLibc > | undefined ; 26 27function openLibc () { 28return Deno . dlopen ( "libc.so.6" , { 29posix_openpt :{ parameters :[ "i32" ], result :"i32" }, 30grantpt :{ parameters :[ "i32" ], result :"i32" }, 31unlockpt :{ parameters :[ "i32" ], result :"i32" }, 32ptsname_r :{ parameters :[ "i32" , "buffer" , "usize" ], result :"i32" }, 33ioctl :{ parameters :[ "i32" , "u64" , "buffer" ], result :"i32" }, 34posix_spawnattr_init :{ parameters :[ "buffer" ], result :"i32" }, 35posix_spawnattr_setflags :{ parameters :[ "buffer" , "i16" ], result :"i32" }, 36posix_spawnattr_destroy :{ parameters :[ "buffer" ], result :"i32" }, 37posix_spawn_file_actions_init :{ parameters :[ "buffer" ], result :"i32" }, 38posix_spawn_file_actions_addopen :{ 39parameters :[ "buffer" , "i32" , "buffer" , "i32" , "u32" ], 40result :"i32" , 41}, 42posix_spawn_file_actions_adddup2 :{ parameters :[ "buffer" , "i32" , "i32" ], result :"i32" }, 43posix_spawn_file_actions_addchdir_np :{ parameters :[ "buffer" , "buffer" ], result :"i32" }, 44posix_spawn_file_actions_destroy :{ parameters :[ "buffer" ], result :"i32" }, 45posix_spawnp :{ 46parameters :[ "buffer" , "buffer" , "buffer" , "buffer" , "buffer" , "buffer" ], 47result :"i32" , 48}, 49__errno_location :{ parameters :[], result :"pointer" }, 50eventfd :{ parameters :[ "u32" , "i32" ], result :"i32" }, 51close :{ parameters :[ "i32" ], result :"i32" }, 52read :{ parameters :[ "i32" , "buffer" , "usize" ], result :"isize" }, 53write :{ parameters :[ "i32" , "buffer" , "usize" ], result :"isize" }, 54// these wait, on the program or for the terminal: each on a thread of its own 55waitpid :{ parameters :[ "i32" , "buffer" , "i32" ], result :"i32" , nonblocking :true }, 56poll :{ parameters :[ "buffer" , "u64" , "i32" ], result :"i32" , nonblocking :true }, 57}). symbols ; 58} 59 60const encoder = new TextEncoder (); 61const cString = ( text :string ) => encoder . encode ( ` ${ text } \0` ); 62 63/** A NULL-terminated array of pointers to C strings, as argv and envp are; the strings live as long as it does. */ 64function cStrings ( texts :readonly string []) { 65const strings = texts . map ( cString ); 66const pointers = new BigUint64Array ( strings . length + 1 ); 67for ( const [ i , string ] of strings . entries ()) { 68pointers [ i ] = BigInt ( Deno . UnsafePointer . value ( Deno . UnsafePointer . of ( string ))); 69} 70return Object . assign ( new Uint8Array ( pointers . buffer ), { strings}); 71} 72 73/** A program on a new pseudo-terminal on this machine, leading a session of its own, `cwd` as given. */ 74export async function ptyHere ( 75argv :readonly string [], 76{ cwd, env, cols, rows} :PtyOptions , 77context :Context , 78) :Promise < Pty > { 79context . abortSignal ?. throwIfAborted (); 80if ( Deno . build . os === "windows" ) return await windowsPty ( argv , { cwd, env, cols, rows}, context ); 81if ( Deno . build . os !== "linux" ) throw new Error ( `No terminals on ${ Deno . build . os } yet` ); 82const c = libc ??= openLibc (); 83// errno belongs to this thread, so only inspect it after synchronous calls 84const errno = new Deno . UnsafePointerView ( c . __errno_location () ! ); 85// it never blocks: waiting is poll's, which a hang-up can cut short 86const master = c . posix_openpt ( O_RDWR | O_NOCTTY | O_NONBLOCK | O_CLOEXEC ); 87if ( master < 0 ) throw new Error ( "Could not open a pseudo-terminal" ); 88const hangingUp = c . eventfd ( 0 , O_NONBLOCK | O_CLOEXEC ); 89const path = new Uint8Array ( 256 ); 90if ( 91hangingUp < 0 || c . grantpt ( master ) || c . unlockpt ( master ) || 92c . ptsname_r ( master , path , BigInt ( path . length )) 93) { 94c . close ( master ); 95if ( hangingUp >= 0 ) c . close ( hangingUp ); 96throw new Error ( "Could not set up a pseudo-terminal" ); 97} 98const resize = ( cols :number , rows :number ) => { 99const size = new Uint16Array ([ rows , cols , 0 , 0 ]); 100c . ioctl ( master , TIOCSWINSZ , new Uint8Array ( size . buffer )); 101}; 102resize ( cols , rows ); 103 104// in a session of its own, the terminal it opens becomes its controlling terminal, which gives it job control 105const attributes = new Uint8Array ( OPAQUE ); 106const actions = new Uint8Array ( OPAQUE ); 107c . posix_spawnattr_init ( attributes ); 108c . posix_spawnattr_setflags ( attributes , POSIX_SPAWN_SETSID ); 109c . posix_spawn_file_actions_init ( actions ); 110c . posix_spawn_file_actions_addopen ( actions , 0 , path , O_RDWR , 0 ); 111c . posix_spawn_file_actions_adddup2 ( actions , 0 , 1 ); 112c . posix_spawn_file_actions_adddup2 ( actions , 0 , 2 ); 113if ( cwd !== undefined ) c . posix_spawn_file_actions_addchdir_np ( actions , cString ( cwd )); 114const variables = Object . entries ({ ...Deno . env . toObject (), ...env }); 115const pid = new Int32Array ( 1 ); 116// it returns once the program has opened the terminal, or failed to 117const failed = c . posix_spawnp ( 118new Uint8Array ( pid . buffer ), 119cString ( argv [ 0 ]), 120actions , 121attributes , 122cStrings ( argv ), 123cStrings ( variables . map (([ key , value ]) => ` ${ key } = ${ value } ` )), 124); 125c . posix_spawn_file_actions_destroy ( actions ); 126c . posix_spawnattr_destroy ( attributes ); 127if ( failed ) { 128c . close ( master ); 129c . close ( hangingUp ); 130throw new Error ( 131failed === ENOENT ?`No such program: ${ argv [ 0 ]} ` :`Could not start ${ argv [ 0 ]} ( ${ failed } )` , 132); 133} 134 135let hungUp = false ; 136let closed = false ; 137/** Polls under way, which would see another descriptor if the terminal's number were reused. */ 138let polling = 0 ; 139let grace :ReturnType < typeof setTimeout > | undefined ; 140const release = () => { 141if ( ! hungUp || polling > 0 || closed ) return ; 142closed = true ; 143c . close ( master ); 144c . close ( hangingUp ); 145}; 146// closing the terminal's side hangs it up for everything that holds the other, and SIGHUPs the program 147const hangUp = () => { 148if ( hungUp ) return ; 149hungUp = true ; 150clearTimeout ( grace ); 151const wake = new Uint8Array ( new BigUint64Array ([ 1n ]). buffer ); 152while ( c . write ( hangingUp , wake , 8n ) < 0 && errno . getInt32 () === EINTR ) { 153// a lost wakeup would leave pending polls holding the terminal open 154} 155release (); 156}; 157/** Wait until the terminal might be ready for `events`, or `false` once it hangs up. */ 158const ready = async ( events :number ) => { 159if ( hungUp ) return false ; 160const fds = new Int32Array ([ master , events , hangingUp , POLLIN ]); 161polling ++ ; 162try { 163// interrupted, it is ready for nothing 164for ( let tries = 0 ; tries < 3 ; tries ++ ) { 165const result = await c . poll ( new Uint8Array ( fds . buffer ), 2n , - 1 ); 166if ( hungUp ) return false ; 167if ( result >= 0 ) return result > 0 && ( fds [ 1 ] >>> 16 ) !== 0 ; 168} 169hangUp (); 170throw new Error ( "Could not wait for the terminal" ); 171} finally { 172polling -- ; 173release (); 174} 175}; 176 177context . abortSignal ?. addEventListener ( "abort" , hangUp , { once :true }); 178const exited = ( async () => { 179const status = new Int32Array ( 1 ); 180// a wait cut short by a signal is tried again; one that keeps failing has nothing left to wait for 181for ( let tries = 0 ; tries < 3 ; tries ++ ) { 182if ( await c . waitpid ( pid [ 0 ], new Uint8Array ( status . buffer ), 0 ) !== pid [ 0 ]) continue ; 183// exited, with a code, or ended by a signal 184return ( status [ 0 ] & 0x7f ) === 0 ?( status [ 0 ] >> 8 ) & 0xff :null ; 185} 186return null ; 187})(). finally (() => { 188context . abortSignal ?. removeEventListener ( "abort" , hangUp ); 189// what it started may hold the terminal open, but as with a window, it goes with the program 190if ( ! hungUp ) grace = setTimeout ( hangUp , GRACE_MS ); 191}); 192 193const readable = new ReadableStream < Uint8Array >({ 194async pull ( controller ) { 195const buffer = new Uint8Array ( 65536 ); 196while ( ! hungUp ) { 197const n = Number ( c . read ( master , buffer , BigInt ( buffer . length ))); 198if ( n > 0 ) return controller . enqueue ( buffer . subarray ( 0 , n )); 199const error = n < 0 ?errno . getInt32 () :0 ; 200// EIO once nothing holds the terminal's other side any more 201if ( n === 0 || error === EIO ) break ; 202if ( error === EINTR ) continue ; 203if ( error !== EAGAIN ) { 204hangUp (); 205throw new Error ( `Could not read from the terminal ( ${ error } )` ); 206} 207if ( ! await ready ( POLLIN )) break ; 208} 209hangUp (); 210controller . close (); 211}, 212cancel :hangUp , 213}, { highWaterMark :0 }); 214const writable = new WritableStream < Uint8Array >({ 215async write ( chunk ) { 216for ( let at = 0 ; at < chunk . length ;) { 217if ( hungUp ) throw new Error ( "The terminal is closed" ); 218const n = Number ( c . write ( master , chunk . subarray ( at ), BigInt ( chunk . length - at ))); 219if ( n > 0 ) { 220at += n ; 221continue ; 222} 223const error = n < 0 ?errno . getInt32 () :0 ; 224if ( error === EINTR ) continue ; 225if ( error !== EAGAIN ) { 226hangUp (); 227throw new Error ( `Could not write to the terminal ( ${ error } )` ); 228} 229if ( ! await ready ( POLLOUT )) throw new Error ( "The terminal is closed" ); 230} 231}, 232}); 233return { 234 readable, 235 writable, 236resize :( cols , rows ) => void ( ! hungUp && resize ( cols , rows )), 237 exited, 238kill :hangUp , 239}; 240}