char-slop/berry
multipurpose self-modifiable agentic harness
git clone https://git.t4t.associates/char-slop/berry
e5ec380
main
1// berry's side of an environment on another machine: the worker it starts there, and the ExecutionEnv that speaks to 2// it. Nothing is retried: a call the connection lost may or may not have happened, so it fails. 3 4import { Buffer } from "node:buffer" ; 5import { dirname , fromFileUrl } from "@std/path" ; 6import type { Context } from "@earendil-works/chord" ; 7import { BACKGROUND_CONTEXT , withAbortSignal } from "@earendil-works/chord/context" ; 8import { 9type BinaryReader , 10type DirReader , 11ExecutionError , 12FileError , 13type FileInfo , 14type FileWatcher , 15type Result , 16type ShellExecOptions , 17type ShellExecResult , 18type ShellOutputInfo , 19type TextLineReader , 20type WatchChange , 21type WatchTarget , 22} from "@earendil-works/pi-durable/env" ; 23import type { 24Environment , 25Process , 26Pty , 27PtyOptions , 28Socket , 29SpawnOptions , 30} from "@berry/api/environment.ts" ; 31import { denoExecutable } from "../deno.ts" ; 32import { 33decode , 34encode , 35type FetchOptions , 36type FetchResponse , 37type Hello , 38lines , 39PROTOCOL , 40type Request , 41type Response , 42} from "./codec.ts" ; 43 44/** 45* Reads a length-prefixed module off stdin and imports it, handing it the rest of stdin. Remote shells (sh, cmd, and 46* powershell alike) pass it through double quotes untouched, as it holds none of `"$\`\\%!`. 47*/ 48export const BOOTSTRAP = [ 49"const r=Deno.stdin.readable.getReader();let b=new Uint8Array(0);" , 50"const take=async(n)=>{while(b.length<n){const c=await r.read();if(c.done)Deno.exit(70);" , 51"const m=new Uint8Array(b.length+c.value.length);m.set(b);m.set(c.value,b.length);b=m}" , 52"const o=b.slice(0,n);b=b.slice(n);return o};" , 53"const n=new DataView((await take(4)).buffer).getUint32(0);" , 54"const s=new TextDecoder().decode(await take(n));" , 55"globalThis.berryStdin={reader:r,buffered:b};" , 56"await import('data:text/javascript;base64,'+s)" , 57]. join ( "" ); 58 59/** How berry starts the worker on `destination`, with `deno` as the far side calls it. */ 60export function sshCommand ( destination :string , deno = "deno" ) :string [] { 61return [ 62"ssh" , 63"-T" , 64"-o" , 65"BatchMode=yes" , 66"-o" , 67"ConnectTimeout=15" , 68"-o" , 69"ServerAliveInterval=15" , 70"-o" , 71"ServerAliveCountMax=3" , 72"--" , 73destination , 74` ${ deno } eval --quiet " ${ BOOTSTRAP } "` , 75]; 76} 77 78let bundled :Promise < string > | undefined ; 79 80/** The worker as a single module that needs nothing but deno, in base64 for the bootstrap. */ 81function workerSource () :Promise < string > { 82bundled ??= ( async () => { 83const worker = fromFileUrl ( new URL ( "./worker.ts" , import . meta. url )); 84const out = await Deno . makeTempDir ({ prefix :"berry-worker-" }); 85try { 86const { success, stderr} = await new Deno . Command ( denoExecutable (), { 87args :[ "bundle" , "--quiet" , "--platform" , "deno" , "--output" , ` ${ out } /worker.js` , worker ], 88cwd :dirname ( worker ), 89env :{ NO_COLOR :"1" }, 90stdout :"null" , 91stderr :"piped" , 92}). output (); 93if ( ! success ) { 94throw new Error ( `could not bundle the worker:\n ${ new TextDecoder (). decode ( stderr )} ` ); 95} 96return Buffer . from ( await Deno . readFile ( ` ${ out } /worker.js` )). toString ( "base64" ); 97} finally { 98await Deno . remove ( out , { recursive :true }); 99} 100})(); 101bundled . catch (() => ( bundled = undefined )); 102return bundled ; 103} 104 105const HELLO_TIMEOUT_MS = 60_000 ; 106const STDERR_KEPT = 4096 ; 107 108const messageOf = ( error :unknown ) => ( error instanceof Error ?error . message :String ( error )); 109 110async function settleWithin ( promise :Promise < unknown >, ms :number ) { 111let timer :ReturnType < typeof setTimeout > | undefined ; 112try { 113await Promise . race ([ 114promise . catch (() => {}), 115new Promise < void >(( resolve ) => timer = setTimeout ( resolve , ms )), 116]); 117} finally { 118clearTimeout ( timer ); 119} 120} 121 122function abortable < T > ( promise :Promise < T >, signal :AbortSignal ) :Promise < T > { 123return new Promise (( resolve , reject ) => { 124const abort = () => { 125signal . removeEventListener ( "abort" , abort ); 126reject ( signal . reason ); 127}; 128signal . addEventListener ( "abort" , abort , { once :true }); 129void promise . then ( 130( value ) => { 131signal . removeEventListener ( "abort" , abort ); 132resolve ( value ); 133}, 134( error ) => { 135signal . removeEventListener ( "abort" , abort ); 136reject ( error ); 137}, 138); 139if ( signal . aborted ) abort (); 140}); 141} 142 143type CallOptions = { 144readonly cwd ?:string ; 145readonly context :Context ; 146/** Receives what the call streams, until it settles or `until` does. */ 147readonly listen ?:( event :unknown ) => void ; 148readonly until ?:Promise < void >; 149}; 150 151/** One worker, and the calls in flight to it. */ 152export class Connection { 153readonly #name:string ; 154readonly #process:Deno . ChildProcess ; 155readonly #writer:WritableStreamDefaultWriter < Uint8Array >; 156readonly #pending= new Map < number , PromiseWithResolvers < unknown >>(); 157readonly #listeners= new Map < number , ( event :unknown ) => void >(); 158readonly #gone= Promise . withResolvers < Error >(); 159readonly #lifetime= new AbortController (); 160 #next= 0 ; 161 #lost:Error | undefined ; 162 163private constructor ( 164name :string , 165child :Deno . ChildProcess , 166writer :WritableStreamDefaultWriter < Uint8Array >, 167readonly hello :Hello , 168) { 169this . #name= name ; 170this . #process= child ; 171this . #writer= writer ; 172} 173 174/** Start the worker with `argv`, which runs the bootstrap on the far side. */ 175static async open ( name :string , argv :readonly string []) :Promise < Connection > { 176const source = await workerSource (); 177const child = new Deno . Command ( argv [ 0 ], { 178args :argv . slice ( 1 ), 179stdin :"piped" , 180stdout :"piped" , 181stderr :"piped" , 182}). spawn (); 183let stderr = "" ; 184const stderrReader = child . stderr . getReader (); 185const errors = ( async () => { 186const decoder = new TextDecoder (); 187try { 188while ( true ) { 189const { done, value} = await stderrReader . read (); 190if ( done ) break ; 191stderr = ( stderr + decoder . decode ( value , { stream :true })). slice ( - STDERR_KEPT ); 192} 193stderr = ( stderr + decoder . decode ()). slice ( - STDERR_KEPT ); 194} finally { 195stderrReader . releaseLock (); 196} 197})(). catch (() => {}); 198const stopErrors = async () => { 199await settleWithin ( errors , 100 ); 200await settleWithin ( Promise . allSettled ([ stderrReader . cancel (). catch (() => {}), errors ]), 100 ); 201}; 202const failed = ( why :string ) => 203new Error ( ` ${ name } : ${ why } ${ stderr . trim () ? `\n ${ stderr . trim ()} ` : "" } ` ); 204 205const writer = child . stdin . getWriter (); 206const length = new Uint8Array ( 4 ); 207new DataView ( length . buffer ). setUint32 ( 0 , source . length ); 208const incoming = lines ( child . stdout . getReader ()); 209let timer :ReturnType < typeof setTimeout > | undefined ; 210try { 211await writer . write ( length ); 212await writer . write ( new TextEncoder (). encode ( source )); 213const first = await Promise . race ([ 214incoming . next (), 215new Promise < never >(( _ , reject ) => { 216timer = setTimeout (() => reject ( new Error ( "timed out" )), HELLO_TIMEOUT_MS ); 217}), 218]); 219if ( first . done ) throw new Error ( "closed" ); 220const hello = ( decode ( first . value ) as { hello ?:Hello }). hello ; 221if ( hello ?. protocol !== PROTOCOL ) { 222throw new Error ( `speaks protocol ${ hello ?. protocol } , not ${ PROTOCOL } ` ); 223} 224const connection = new Connection ( name , child , writer , hello ); 225void connection . #receive( incoming , stopErrors , failed ); 226return connection ; 227} catch ( error ) { 228try { 229child . kill (); 230} catch { 231// already gone 232} 233await child . status ; 234await stopErrors (); 235throw failed ( `could not start its worker: ${ messageOf ( error )} ` ); 236} finally { 237clearTimeout ( timer ); 238} 239} 240 241/** Why calls fail, once the worker cannot be reached. */ 242get lost () :Error | undefined { 243return this . #lost; 244} 245 246/** Settles with why, once the worker cannot be reached. */ 247get gone () :Promise < Error > { 248return this . #gone. promise ; 249} 250 251get lifetime () :AbortSignal { 252return this . #lifetime. signal ; 253} 254 255async #receive( 256incoming :AsyncGenerator < string >, 257stopErrors :() => Promise < void >, 258failed :( why :string ) => Error , 259) { 260try { 261for await ( const line of incoming ) { 262const message = decode ( line ) as Response ; 263if ( "hello" in message ) continue ; 264if ( "o" in message ) { 265this . #listeners. get ( message . i ) ?.( message . o ); 266continue ; 267} 268const pending = this . #pending. get ( message . i ); 269this . #pending. delete ( message . i ); 270if ( "e" in message ) pending ?. reject ( new Error ( ` ${ this . #name } : ${ message . e } ` )); 271else pending ?. resolve ( message . r ); 272} 273} catch ( error ) { 274this . #lost= failed ( `lost: ${ messageOf ( error )} ` ); 275} 276await this . #process. status ; 277await stopErrors (); 278this . #lost??= failed ( "lost" ); 279this . #lifetime. abort ( this . #lost); 280this . #gone. resolve ( this . #lost); 281for ( const pending of this . #pending. values ()) pending . reject ( this . #lost); 282this . #pending. clear (); 283this . #listeners. clear (); 284} 285 286 #send( request :Request ) { 287// a failed write means the worker is gone, which #receive reports 288void this . #writer. write ( new TextEncoder (). encode ( ` ${ encode ( request )} \n` )). catch (() => {}); 289} 290 291async call ( method :string , args :readonly unknown [], options :CallOptions ) :Promise < unknown > { 292if ( this . #lost) throw this . #lost; 293const i = ++ this . #next; 294const pending = Promise . withResolvers < unknown >(); 295this . #pending. set ( i , pending ); 296if ( options . listen ) { 297const listen = options . listen ; 298this . #listeners. set ( i , listen ); 299void ( options . until ?? pending . promise ). catch (() => {}). finally (() => { 300if ( this . #listeners. get ( i ) === listen ) this . #listeners. delete ( i ); 301}); 302} 303const signal = options . context . abortSignal ; 304const abort = () => this . #send({ c :i }); 305signal ?. addEventListener ( "abort" , abort , { once :true }); 306this . #send({ 307 i, 308m :method , 309a :args , 310 ...( options . cwd === undefined ?{} :{ cwd :options . cwd }), 311}); 312if ( signal ?. aborted ) abort (); 313try { 314return await pending . promise ; 315} finally { 316signal ?. removeEventListener ( "abort" , abort ); 317} 318} 319 320/** End the worker, and with it everything it runs. */ 321async close () :Promise < void > { 322await this . #writer. close (). catch (() => {}); 323const timer = setTimeout (() => { 324try { 325this . #process. kill (); 326} catch { 327// already gone 328} 329}, 5_000 ); 330await this . #process. status ; 331clearTimeout ( timer ); 332await this . #gone. promise ; 333} 334} 335 336type Calls = ( method :string , args :readonly unknown [], options :CallOptions ) => Promise < unknown >; 337 338const fileFailure = ( error :unknown ) => ({ 339ok :false as const , 340error :new FileError ( "unknown" , messageOf ( error )), 341}); 342// a call already aborted must not happen at all, which only this side can be sure of 343const fileAborted = { ok :false as const , error :new FileError ( "aborted" , "aborted" ) }; 344 345/** A machine berry reaches through a worker there, at `cwd`. */ 346export class RemoteEnvironment implements Environment { 347readonly id :string ; 348readonly platform :string ; 349readonly home :string ; 350readonly variables :Readonly < Record < string , string >>; 351readonly lifetime :AbortSignal ; 352readonly #connection:Connection ; 353/** What `cleanup` stops: the calls made through this object that still run. */ 354readonly #running= new Set < AbortController >(); 355 356constructor ( readonly name :string , connection :Connection , public cwd :string ) { 357this . id = `ssh: ${ name } ` ; 358this . platform = connection . hello . platform ; 359this . home = connection . hello . home ; 360this . variables = connection . hello . variables ; 361this . lifetime = connection . lifetime ; 362this . #connection= connection ; 363} 364 365/** A call that `cleanup` can stop, failing as the environment's calls fail. */ 366 #call:Calls = async ( method , args , options ) => { 367const stop = new AbortController (); 368this . #running. add ( stop ); 369const signals = [ 370stop . signal , 371 ...( options . context . abortSignal ?[ options . context . abortSignal ] :[]), 372]; 373try { 374return await this . #connection. call ( method , args , { 375 ...options , 376context :withAbortSignal ( AbortSignal . any ( signals ), options . context ), 377}); 378} finally { 379this . #running. delete ( stop ); 380} 381}; 382 383async #file< T > ( method :string , args :readonly unknown [], context :Context ) { 384if ( context . abortSignal ?. aborted ) return fileAborted ; 385try { 386return await this . #call( method , args , { cwd :this . cwd , context}) as Result < T , FileError >; 387} catch ( error ) { 388return fileFailure ( error ); 389} 390} 391 392/** Calls on something the worker holds open for this one. */ 393 #held( handle :number ) { 394return async < T > ( method :string , args :readonly unknown [], context :Context ) => { 395if ( context . abortSignal ?. aborted ) return fileAborted as T ; 396try { 397return await this . #call( "handle" , [ handle , method , ...args ], { context}) as T ; 398} catch ( error ) { 399return fileFailure ( error ) as T ; 400} 401}; 402} 403 404absolutePath = ( path :string , context :Context ) => 405this . #file< string >( "absolutePath" , [ path ], context ); 406joinPath = ( parts :string [], context :Context ) => 407this . #file< string >( "joinPath" , [ parts ], context ); 408readTextFile = ( path :string , context :Context ) => 409this . #file< string >( "readTextFile" , [ path ], context ); 410readTextLines = ( path :string , options :{ maxLines ?:number } | undefined , context :Context ) => 411this . #file< string []>( "readTextLines" , [ path , options ], context ); 412readBinaryFile = ( path :string , context :Context ) => 413this . #file< Uint8Array >( "readBinaryFile" , [ path ], context ); 414writeFile = ( path :string , content :string | Uint8Array , context :Context ) => 415this . #file< void >( "writeFile" , [ path , content ], context ); 416appendFile = ( path :string , content :string | Uint8Array , context :Context ) => 417this . #file< void >( "appendFile" , [ path , content ], context ); 418truncateFile = ( path :string , size :number , context :Context ) => 419this . #file< void >( "truncateFile" , [ path , size ], context ); 420flushFile = ( path :string , context :Context ) => this . #file< void >( "flushFile" , [ path ], context ); 421renameFile = ( from :string , to :string , context :Context ) => 422this . #file< void >( "renameFile" , [ from , to ], context ); 423fileInfo = ( path :string , context :Context ) => this . #file< FileInfo >( "fileInfo" , [ path ], context ); 424listDir = ( path :string , context :Context ) => this . #file< FileInfo []>( "listDir" , [ path ], context ); 425canonicalPath = ( path :string , context :Context ) => 426this . #file< string >( "canonicalPath" , [ path ], context ); 427exists = ( path :string , context :Context ) => this . #file< boolean >( "exists" , [ path ], context ); 428createDir = ( path :string , options :{ recursive ?:boolean } | undefined , context :Context ) => 429this . #file< void >( "createDir" , [ path , options ], context ); 430remove = ( 431path :string , 432options :{ recursive ?:boolean ; force ?:boolean } | undefined , 433context :Context , 434) => this . #file< void >( "remove" , [ path , options ], context ); 435createTempDir = ( prefix :string | undefined , context :Context ) => 436this . #file< string >( "createTempDir" , [ prefix ], context ); 437createTempFile = ( 438options :{ prefix ?:string ; suffix ?:string } | undefined , 439context :Context , 440) => this . #file< string >( "createTempFile" , [ options ], context ); 441 442async openTextLineReader ( 443path :string , 444context :Context , 445) :Promise < Result < TextLineReader , FileError >> { 446const opened = await this . #file<{ handle :number }>( "openTextLineReader" , [ path ], context ); 447if ( ! opened . ok ) return opened ; 448const held = this . #held( opened . value . handle ); 449return { 450ok :true , 451value :{ 452readLine :( context ) => held ( "readLine" , [], context ), 453close :( context ) => held ( "close" , [], context ), 454}, 455}; 456} 457 458async openBinaryReader ( 459path :string , 460options :{ noFollow ?:boolean } | undefined , 461context :Context , 462) :Promise < Result < BinaryReader , FileError >> { 463const opened = await this . #file<{ handle :number }>( 464"openBinaryReader" , 465[ path , options ], 466context , 467); 468if ( ! opened . ok ) return opened ; 469const held = this . #held( opened . value . handle ); 470return { 471ok :true , 472value :{ 473info :( context ) => held ( "info" , [], context ), 474read :( offset , length , context ) => held ( "read" , [ offset , length ], context ), 475scanLines :( options , context ) => held ( "scanLines" , [ options ], context ), 476close :( context ) => held ( "close" , [], context ), 477}, 478}; 479} 480 481async openDirReader ( path :string , context :Context ) :Promise < Result < DirReader , FileError >> { 482const opened = await this . #file<{ handle :number }>( "openDirReader" , [ path ], context ); 483if ( ! opened . ok ) return opened ; 484const held = this . #held( opened . value . handle ); 485return { 486ok :true , 487value :{ 488next :( count , context ) => held ( "next" , [ count ], context ), 489close :( context ) => held ( "close" , [], context ), 490}, 491}; 492} 493 494async watch ( 495targets :readonly WatchTarget [], 496onChange :( change :WatchChange ) => void , 497context :Context , 498) :Promise < Result < FileWatcher , FileError >> { 499const closed = Promise . withResolvers < void >(); 500let opened ; 501try { 502opened = await this . #connection. call ( "watch" , [ targets ], { 503cwd :this . cwd , 504 context, 505listen :( change ) => onChange ( change as WatchChange ), 506until :closed . promise , 507}) as Result <{ handle :number ; mode :FileWatcher [ "mode" ] }, FileError >; 508} catch ( error ) { 509opened = fileFailure ( error ); 510} 511if ( ! opened . ok ) { 512closed . resolve (); 513return opened ; 514} 515const held = this . #held( opened . value . handle ); 516return { 517ok :true , 518value :{ 519mode :opened . value . mode , 520async close ( context ) { 521await held ( "close" , [], context ); 522closed . resolve (); 523}, 524}, 525}; 526} 527 528async exec ( 529command :string | readonly string [], 530options :ShellExecOptions | undefined , 531context :Context , 532) :Promise < Result < ShellExecResult , ExecutionError >> { 533if ( context . abortSignal ?. aborted ) { 534return { ok :false , error :new ExecutionError ( "aborted" , "aborted" ) }; 535} 536const { onOutput, ...rest } = options ?? {}; 537// a throwing callback stops the command, as it would here 538const failing = new AbortController (); 539let failure :unknown ; 540const listen = onOutput && 541(( event :unknown ) => { 542const [ text , info ] = event as [ string , ShellOutputInfo ]; 543try { 544onOutput ( text , context , info ); 545} catch ( error ) { 546failure ??= error ; 547failing . abort (); 548} 549}); 550try { 551const result = await this . #call( "exec" , [ command , { ...rest , output :!! onOutput }], { 552cwd :this . cwd , 553context :withAbortSignal ( 554AbortSignal . any ([ failing . signal , ...( context . abortSignal ?[ context . abortSignal ] :[])]), 555context , 556), 557 ...( listen ?{ listen} :{}), 558}) as Result < ShellExecResult , ExecutionError >; 559if ( failure === undefined ) return result ; 560return { ok :false , error :new ExecutionError ( "callback_error" , messageOf ( failure )) }; 561} catch ( error ) { 562return { ok :false , error :new ExecutionError ( "unknown" , messageOf ( error )) }; 563} 564} 565 566async fetch ( input :string | URL , init ?:RequestInit ) :Promise < globalThis . Response > { 567init ?. signal ?. throwIfAborted (); 568const request = new globalThis . Request ( input , init ); 569const stop = new AbortController (); 570this . #running. add ( stop ); 571const signal = AbortSignal . any ([ stop . signal , request . signal ]); 572const during = withAbortSignal ( signal , BACKGROUND_CONTEXT ); 573let opened :FetchResponse | undefined ; 574let controller :ReadableStreamDefaultController < Uint8Array > | undefined ; 575let ended = false ; 576const finish = () => { 577ended = true ; 578this . #running. delete ( stop ); 579signal . removeEventListener ( "abort" , abort ); 580}; 581const release = async ( handle :number | undefined ) => { 582if ( handle !== undefined ) { 583await abortable ( 584this . #connection. call ( "handle" , [ handle , "close" ], { 585context :BACKGROUND_CONTEXT , 586}), 587AbortSignal . timeout ( 1000 ), 588). catch (() => {}); 589} 590}; 591const close = async () => { 592if ( ended ) return ; 593finish (); 594await release ( opened ?. handle ); 595}; 596const abort = () => { 597if ( ended ) return ; 598controller ?. error ( signal . reason ); 599void close (); 600}; 601try { 602const options :FetchOptions = { 603method :request . method , 604headers :[ ...request . headers ], 605 ...( request . body 606 ?{ body :new Uint8Array ( await abortable ( request . arrayBuffer (), signal )) } 607 :{}), 608redirect :request . redirect , 609credentials :request . credentials , 610cache :request . cache , 611referrer :request . referrer , 612referrerPolicy :request . referrerPolicy , 613integrity :request . integrity , 614}; 615signal . throwIfAborted (); 616const opening = this . #connection. call ( "fetch" , [ request . url , options ], { 617context :during , 618}). then (( value ) => { 619opened = value as FetchResponse ; 620// a canceled caller must still release a handle returned late by the worker 621if ( ended ) void release ( opened . handle ); 622return opened ; 623}); 624opened = await abortable ( opening , signal ); 625signal . throwIfAborted (); 626const body = opened . handle === undefined ?null :new ReadableStream < Uint8Array >({ 627start :( stream ) => { 628controller = stream ; 629signal . addEventListener ( "abort" , abort , { once :true }); 630}, 631pull :async ( stream ) => { 632try { 633const next = await abortable ( 634this . #connection. call ( "handle" , [ opened ! . handle , "read" ], { 635context :during , 636}), 637signal , 638) as ReadableStreamReadResult < Uint8Array >; 639if ( ended ) return ; 640if ( next . done ) { 641finish (); 642stream . close (); 643} else stream . enqueue ( next . value ); 644} catch ( error ) { 645if ( ! ended ) { 646stream . error ( signal . aborted ?signal . reason :error ); 647void close (); 648} 649} 650}, 651cancel :close , 652}); 653if ( body === null ) finish (); 654const response = new globalThis . Response ( body , opened ); 655Object . defineProperties ( response , { 656url :{ value :opened . url }, 657redirected :{ value :opened . redirected }, 658}); 659return response ; 660} catch ( error ) { 661void close (); 662if ( signal . aborted ) throw signal . reason ; 663throw new TypeError ( `Fetch failed on ${ this . name } : ${ messageOf ( error )} ` , { cause :error }); 664} 665} 666 667async spawn ( argv :readonly string [], options :SpawnOptions , context :Context ) :Promise < Process > { 668return ( await this . #pipe( "spawn" , [ argv , options ], context )). process ; 669} 670 671async pty ( argv :readonly string [], options :PtyOptions , context :Context ) :Promise < Pty > { 672const { process, operate} = await this . #pipe( "pty" , [ argv , options ], context ); 673return { 674readable :process . stdout , 675writable :process . stdin , 676resize :( cols , rows ) => void operate ( "resize" , cols , rows ). catch (() => {}), 677exited :process . exited , 678kill :process . kill , 679}; 680} 681 682async connect ( hostname :string , port :number , context :Context ) :Promise < Socket > { 683const { stdin, stdout, kill} = 684( await this . #pipe( "connect" , [ hostname , port ], context )). process ; 685return { readable :stdout , writable :stdin , close :kill }; 686} 687 688/** Something the worker runs or holds open, spoken to through its pipes until it exits. */ 689async #pipe( 690method :string , 691args :readonly unknown [], 692context :Context , 693) :Promise <{ process :Process ; operate ( operation :string , ...rest :unknown []) :Promise < void > }> { 694context . abortSignal ?. throwIfAborted (); 695const streams :Record < string , ReadableStreamDefaultController < Uint8Array >> = {}; 696const readable = ( name :string ) => 697new ReadableStream < Uint8Array >({ 698start :( controller ) => void ( streams [ name ] = controller ), 699cancel :() => void delete streams [ name ], 700}); 701const stdout = readable ( "stdout" ); 702const stderr = readable ( "stderr" ); 703const exited = Promise . withResolvers < number | null >(); 704const finished = Promise . withResolvers < void >(); 705const end = ( error ?:Error ) => { 706for ( const stream of Object . values ( streams )) { 707try { 708if ( error ) stream . error ( error ); 709else stream . close (); 710} catch { 711// already ended 712} 713} 714}; 715const listen = ( event :unknown ) => { 716const [ name , chunk ] = event as [ string , Uint8Array | number | null ]; 717if ( name === "exit" ) exited . resolve ( chunk as number | null ); 718else if ( name === "end" ) { 719end (); 720finished . resolve (); 721} else if ( chunk === null ) { 722streams [ name ]?. close (); 723delete streams [ name ]; 724} else streams [ name ]?. enqueue ( chunk as Uint8Array ); 725}; 726let opened :{ pipe :number }; 727try { 728opened = await this . #connection. call ( method , args , { 729cwd :this . cwd , 730 context, 731 listen, 732until :finished . promise , 733}) as { pipe :number }; 734} catch ( error ) { 735exited . resolve ( null ); 736finished . resolve (); 737throw error ; 738} 739// what berry cannot reach any more has ended, as far as anyone here can tell 740void Promise . race ([ this . #connection. gone , finished . promise ]). then (( outcome ) => { 741if ( outcome instanceof Error ) { 742end ( outcome ); 743exited . resolve ( null ); 744finished . resolve (); 745} 746}); 747const operate = ( operation :string , ...rest :unknown []) => 748this . #connection. call ( "pipe" , [ opened . pipe , operation , ...rest ], { 749context :BACKGROUND_CONTEXT , 750}). then (() => {}); 751const kill = ( signal :"SIGTERM" | "SIGKILL" = "SIGTERM" ) => 752void operate ( "kill" , signal ). catch (() => {}); 753const abort = () => kill (); 754context . abortSignal ?. addEventListener ( "abort" , abort , { once :true }); 755if ( context . abortSignal ?. aborted ) abort (); 756void exited . promise . then (() => context . abortSignal ?. removeEventListener ( "abort" , abort )); 757const process = { 758stdin :new WritableStream < Uint8Array >({ 759write :( chunk ) => operate ( "write" , chunk ), 760close :() => operate ( "end" ), 761abort :() => operate ( "kill" ), 762}), 763 stdout, 764 stderr, 765exited :exited . promise , 766 kill, 767}; 768return { process, operate}; 769} 770 771cleanup ( _context :Context ) :Promise < void > { 772for ( const stop of this . #running) stop . abort (); 773return Promise . resolve (); 774} 775}