char/avril
(wip) why'd you have to go and make things so complicaaaaated?
git clone https://git.t4t.associates/char/avril
38d7f6c
main
1import { 2ArrayPrototypePush as push , 3Error , 4FinalizationRegistry , 5ObjectCreate , 6ObjectDefineProperty as define , 7ObjectFreeze as freeze , 8ObjectGetOwnPropertyDescriptor , 9RangeError , 10ReflectApply as apply , 11ReflectOwnKeys , 12Set , 13SetPrototypeAdd as retain , 14SetPrototypeDelete as release , 15String , 16StringPrototypeSlice as slice , 17SymbolIterator , 18SymbolToStringTag , 19TypedArrayPrototypeGetBuffer as buffer , 20TypedArrayPrototypeGetByteLength as byteLength , 21TypedArrayPrototypeGetByteOffset as byteOffset , 22TypedArrayPrototypeGetTag as tag , 23TypeError , 24Uint8Array , 25Uint8ArrayPrototypeSet as setBytes , 26uncurryThis , 27WeakMap , 28WeakMapPrototypeSet as set , 29} from "ext:core/primordials" ; 30import { 31markPromiseHandled , 32promiseAll , 33promiseResolve , 34promiseThen as then , 35safePromise , 36} from "ext:core/promises" ; 37import { 38op_process_cancel_read , 39op_process_end , 40op_process_keepalive , 41op_process_kill , 42op_process_read , 43op_process_ref , 44op_process_release , 45op_process_resize , 46op_process_run_sync , 47op_process_spawn , 48op_process_stop , 49op_process_wait , 50op_process_write , 51} from "ext:core/ops" ; 52import { 53array , 54dictionary , 55isObject , 56required , 57state , 58usv , 59} from "ext:web/idl.js" ; 60import { type AbortSignal , addAbort , signals } from "ext:web/events.js" ; 61import { registerUncloneable } from "ext:web/serialization.js" ; 62import type { ReadableStreamDefaultReader } from "ext:webstreams/streams.js" ; 63import { 64enqueue , 65finish , 66read , 67ReadableStream , 68readerCancel , 69release as releaseReader , 70streamLocked , 71streamReader , 72WritableStream , 73} from "ext:webstreams/streams.js" ; 74import { TextDecoder , TextEncoder } from "ext:encoding/main.js" ; 75import { args , path , processHandle } from "ext:file/main.js" ; 76import { addSignalListener , removeSignalListener } from "./signals.js" ; 77import { errorCode , failure , result } from "ext:file/errors.js" ; 78import type { URL } from "ext:url/main.js" ; 79import type { ProcessOptions , RunSyncOptions , SpawnOptions } from "avril:std" ; 80import type { 81NativeOptions , 82NativeStdio , 83ProcessBytes , 84ProcessOutputValue , 85ProcessStatusValue , 86} from "./types.js" ; 87 88const Number = globalThis . Number , 89safeInteger = Number . isSafeInteger , 90min = Math . min ; 91const decode = uncurryThis ( TextDecoder . prototype . decode ), 92encode = uncurryThis ( TextEncoder . prototype . encode ), 93encodingOf = uncurryThis ( 94ObjectGetOwnPropertyDescriptor ( TextDecoder . prototype , "encoding" ) ! . get ! , 95); 96const encoder = new TextEncoder (), utf8 = new TextDecoder (); 97const parseJSON = JSON . parse , 98indexOf = uncurryThis ( String . prototype . indexOf ); 99const writerOf = uncurryThis ( WritableStream . prototype . getWriter ), 100write = uncurryThis ( WritableStreamDefaultWriter . prototype . write ), 101closeWriter = uncurryThis ( WritableStreamDefaultWriter . prototype . close ), 102releaseWriter = uncurryThis ( 103WritableStreamDefaultWriter . prototype . releaseLock , 104); 105const asyncDispose :typeof Symbol . asyncDispose = 106globalThis . Symbol . asyncDispose ; 107const children = new WeakMap < ChildProcess , ProcessRecord >(), 108live = new Set < ChildProcess >(), 109collecting = new Set < ChildProcess >(), 110secret = ObjectCreate ( null ) as Secret ; 111registerUncloneable ( children ); 112const register = uncurryThis ( FinalizationRegistry . prototype . register ), 113unregister = uncurryThis ( FinalizationRegistry . prototype . unregister ); 114const finalizer = new FinalizationRegistry (( id :number ) => 115op_process_release ( id ) 116); 117 118type ProcessRecord < T = ProcessBytes | string > = { 119__proto__ ?:null ; 120id :number ; 121pid :number ; 122reads :Record < number , ReadSource | undefined >; 123writes :Record < number , WriteSource | undefined >; 124stdio :Record < number , ProcessStream >; 125terminal :ProcessTerminal | null ; 126collectLimit :number ; 127decoder :TextDecoder | null ; 128decode :( bytes :ProcessBytes ) => T ; 129encoding :string ; 130check :boolean ; 131automaticInput :boolean ; 132collected :boolean ; 133disposed :boolean ; 134finished :boolean ; 135referenced :boolean ; 136inputTask :Promise < void > | undefined ; 137inputFailed :boolean ; 138inputError :unknown ; 139abortReason :unknown ; 140status :Promise < ProcessStatusValue >; 141completed :Promise < ProcessStatusValue >; 142}; 143type ProcessStream = 144| ReadableStream < ProcessBytes > 145| WritableStream < ProcessBytes >; 146type ReadSource = { 147__proto__ ?:null ; 148owner :ChildProcess | null ; 149used :boolean ; 150stream :ReadableStream < ProcessBytes >; 151}; 152type WriteSource = { 153__proto__ ?:null ; 154owner :ChildProcess | null ; 155stream :WritableStream < ProcessBytes >; 156}; 157type ProcessTerminal = { 158readable :ReadableStream < ProcessBytes >; 159writable :WritableStream < ProcessBytes >; 160resize ( columns :number , rows :number ) :void ; 161}; 162type CollectionBudget = { 163__proto__ ?:null ; 164limit :number ; 165used :number ; 166exceeded :boolean ; 167}; 168type Collection = { 169__proto__ ?:null ; 170status :ProcessStatusValue ; 171stdout :ProcessBytes | null ; 172stderr :ProcessBytes | null ; 173cause :unknown ; 174}; 175type LaunchedStream = { 176__proto__ ?:null ; 177record :ProcessRecord ; 178reader :ReadableStreamDefaultReader < ProcessBytes >; 179settled :Promise < Collection >; 180}; 181interface ProcessPromise < T = ProcessBytes | string > 182extends Promise < ProcessOutputValue < T >> { 183bytes () :Promise < ProcessBytes >; 184text () :Promise < string >; 185json () :Promise < unknown >; 186lines () :AsyncIterableIterator < string >; 187} 188type Prepared < T = ProcessBytes | string > = { 189__proto__ ?:null ; 190native :NativeOptions ; 191input :ProcessBytes | ReadableStream < ProcessBytes > | undefined ; 192stream :boolean ; 193check :boolean ; 194decoder :TextDecoder | null ; 195decode :( bytes :ProcessBytes ) => T ; 196signal :unknown ; 197maxCollectedBytes :number ; 198}; 199 200function own ( object :object , key :PropertyKey , value :unknown ) :void { 201define ( object , key , { 202__proto__ :null , 203 value, 204writable :true , 205configurable :true , 206enumerable :true , 207} as PropertyDescriptor ); 208} 209class ProcessError extends Error { 210declare static readonly [ Symbol . hasInstance ] :( value :unknown ) => value isProcessError ; 211declare readonly result :ProcessOutputValue | null ; 212 213constructor ( 214message :string , 215output :ProcessOutputValue | null = null , 216cause :unknown = undefined , 217) { 218super ( message ); 219own ( this , "name" , "ProcessError" ); 220own ( this , "result" , output ); 221if ( cause !== undefined ) own ( this , "cause" , cause ); 222} 223} 224class OutputLimitError extends ProcessError { 225declare static readonly [ Symbol . hasInstance ] :( value :unknown ) => value isOutputLimitError ; 226constructor ( output :ProcessOutputValue ) { 227super ( "Subprocess output exceeded its configured limit" , output ); 228own ( this , "name" , "OutputLimitError" ); 229} 230} 231class ProcessTimeoutError extends ProcessError { 232declare static readonly [ Symbol . hasInstance ] :( value :unknown ) => value isProcessTimeoutError ; 233constructor ( output :ProcessOutputValue ) { 234super ( "Subprocess timed out" , output ); 235own ( this , "name" , "ProcessTimeoutError" ); 236} 237} 238class ProcessAbortError extends ProcessError { 239declare static readonly [ Symbol . hasInstance ] :( value :unknown ) => value isProcessAbortError ; 240constructor ( output :ProcessOutputValue | null , cause ?:unknown ) { 241super ( "Subprocess was aborted" , output , cause ); 242own ( this , "name" , "ProcessAbortError" ); 243} 244} 245function checked < T extends Uint8Array | string > ( 246output :ProcessOutputValue < T >, 247check :boolean , 248cause ?:unknown , 249) :ProcessOutputValue < T > { 250if ( output . reason === "output-limit" ) throw new OutputLimitError ( output ); 251if ( output . reason === "timeout" ) throw new ProcessTimeoutError ( output ); 252if ( output . reason === "abort" ) throw new ProcessAbortError ( output , cause ); 253if ( output . reason !== null || ( check && ! output . success )) { 254throw new ProcessError ( 255output . error || 256( output . reason === null 257 ?`Subprocess exited with ${ output . signal || output . code } ` 258 :`Subprocess ${ output . reason } ` ), 259output , 260cause , 261); 262} 263return output ; 264} 265function keys ( 266value :Record < PropertyKey , unknown >, 267allowed :readonly PropertyKey [], 268) :void { 269const names = ReflectOwnKeys ( value ); 270for ( let i = 0 ; i < names . length ; i ++ ) { 271let found = false ; 272for ( let j = 0 ; j < allowed . length ; j ++ ) { 273if ( names [ i ] === allowed [ j ]) found = true ; 274} 275if ( ! found ) throw new TypeError ( "Unknown subprocess option" ); 276} 277} 278function integer < F extends number | null | undefined > ( 279value :unknown , 280fallback :F , 281minimum = 0 , 282maximum = 2 ** 53 - 1 , 283) :number | Extract < F , null | undefined > { 284if ( value === undefined ) { 285return fallback as number | Extract < F , null | undefined >; 286} 287if ( 288typeof value !== "number" || ! safeInteger ( value ) || value < minimum || 289value > maximum 290) throw new RangeError ( "Subprocess option is outside its integer range" ); 291return value ; 292} 293function boolean ( value :unknown , fallback :boolean ) :boolean { 294if ( value === undefined ) return fallback ; 295if ( typeof value !== "boolean" ) { 296throw new TypeError ( "Expected a boolean subprocess option" ); 297} 298return value ; 299} 300function bytes ( value :unknown , text = false ) :ProcessBytes { 301if ( text && typeof value === "string" ) return encode ( encoder , value ); 302if ( tag ( value ) !== "Uint8Array" ) { 303throw new TypeError ( "Subprocess input requires string or Uint8Array bytes" ); 304} 305const copy = new Uint8Array ( byteLength ( value as Uint8Array < ArrayBufferLike >)); 306setBytes ( copy , value as Uint8Array < ArrayBufferLike >); 307return copy ; 308} 309function sequence ( value :unknown ) :string [] { 310const output = array < string >(); 311if ( value === undefined ) return output ; 312if ( ! isObject ( value )) { 313throw new TypeError ( "Arguments must be an iterable of strings" ); 314} 315const method = value [ SymbolIterator ]; 316if ( typeof method !== "function" ) { 317throw new TypeError ( "Arguments must be iterable" ); 318} 319const iterator = apply ( method , value , []) as { next :unknown }, 320next = iterator . next ; 321if ( ! isObject ( iterator ) || typeof next !== "function" ) { 322throw new TypeError ( "Invalid argument iterator" ); 323} 324while ( true ) { 325const step = apply ( next , iterator , []); 326if ( ! isObject ( step )) { 327throw new TypeError ( "Invalid argument iterator result" ); 328} 329if ( step . done ) return output ; 330push ( output , usv ( step . value )); 331} 332} 333function stdio ( 334value :unknown , 335fallback :string , 336input :boolean , 337stderr = false , 338extra = false , 339) :NativeStdio { 340const spec :NativeStdio = { 341__proto__ :null , 342mode :fallback , 343file :null , 344fileMode :null , 345handle :null , 346direction :null , 347}; 348if ( typeof value === "string" || value === undefined ) { 349if ( value !== undefined ) spec . mode = value ; 350} else { 351const options = dictionary ( value ); 352keys ( options , [ "file" , "mode" , "handle" , "pipe" , "direction" ]); 353const filename = options . file , 354mode = options . mode , 355handle = options . handle , 356pipe = options . pipe , 357direction = options . direction ; 358if ( extra ) { 359spec . direction = ( pipe === undefined ?direction :pipe ) as string ; 360input = spec . direction === "parent-to-child" ; 361} 362if ( filename !== undefined ) { 363if ( handle !== undefined || pipe !== undefined ) { 364throw new TypeError ( "Conflicting stdio options" ); 365} 366spec . mode = "file" ; 367spec . file = path ( filename ); 368if ( input ) { 369if ( mode !== undefined ) { 370throw new TypeError ( "Input redirection has no file mode" ); 371} 372} else spec . fileMode = mode === undefined ?"truncate" :usv ( mode ); 373} else if ( handle !== undefined ) { 374if ( pipe !== undefined || mode !== undefined ) { 375throw new TypeError ( "Conflicting stdio handle options" ); 376} 377spec . mode = "handle" ; 378spec . handle = processHandle ( handle ); 379} else if ( extra && pipe !== undefined ) { 380if ( mode !== undefined || direction !== undefined ) { 381throw new TypeError ( "Conflicting pipe options" ); 382} 383spec . mode = "piped" ; 384} else throw new TypeError ( "Expected a stdio mode, file, or handle" ); 385} 386if ( 387spec . mode !== "piped" && spec . mode !== "inherit" && spec . mode !== "null" && 388spec . mode !== "file" && spec . mode !== "handle" && 389! ( stderr && spec . mode === "stdout" ) 390) throw new TypeError ( "Invalid stdio mode" ); 391if ( extra && spec . mode === "inherit" ) { 392throw new TypeError ( "Extra stdio cannot inherit internal descriptors" ); 393} 394if ( 395extra && spec . direction !== "parent-to-child" && 396spec . direction !== "child-to-parent" 397) throw new TypeError ( "Extra stdio needs a pipe direction" ); 398if ( 399spec . fileMode !== null && spec . fileMode !== "append" && 400spec . fileMode !== "truncate" && spec . fileMode !== "create-new" 401) throw new TypeError ( "Invalid stdio file mode" ); 402return spec ; 403} 404/** Splits `(args?, options?)`: an iterable second argument is the argument list. */ 405function invocation ( 406second :unknown , 407third :unknown , 408) :{ __proto__ ?:null ; args :unknown ; options :unknown } { 409if ( isObject ( second ) && typeof second [ SymbolIterator ] === "function" ) { 410return { __proto__ :null , args :second , options :third }; 411} 412if ( 413typeof second === "string" || ( second !== undefined && third !== undefined ) 414) { 415throw new TypeError ( "Arguments must be an iterable of strings" ); 416} 417return { __proto__ :null , args :undefined , options :second ?? third }; 418} 419function normalize ( 420command :unknown , 421positional :unknown , 422raw :unknown , 423capture :boolean , 424sync :boolean , 425) :Prepared { 426command = path ( command ); 427const options = dictionary ( raw ); 428keys ( options , [ 429"args" , 430"cwd" , 431"searchPath" , 432"clearEnv" , 433"env" , 434"stdin" , 435"stdout" , 436"stderr" , 437"input" , 438"encoding" , 439"check" , 440"signal" , 441"timeoutMs" , 442"maxCollectedBytes" , 443"maxBufferedBytes" , 444"termination" , 445"drainTimeoutMs" , 446"extraStdio" , 447"terminal" , 448]); 449if ( positional !== undefined && options . args !== undefined ) { 450throw new TypeError ( "Arguments were given both positionally and as args" ); 451} 452const argv = sequence ( positional ?? options . args ), cwdValue = options . cwd ; 453const cwd = cwdValue === undefined ?null :path ( cwdValue ); 454const searchPath = boolean ( options . searchPath , true ), 455clearEnv = boolean ( options . clearEnv , false ); 456const environment = dictionary ( options . env ), 457env = array <[ string , string ]>(), 458names = ReflectOwnKeys ( environment ); 459for ( let i = 0 ; i < names . length ; i ++ ) { 460const key = names [ i ]; 461if ( typeof key !== "string" ) { 462throw new TypeError ( "Environment names must be strings" ); 463} 464if ( ObjectGetOwnPropertyDescriptor ( environment , key ) ! . enumerable ) { 465push ( env , args ( key , usv ( environment [ key ]))); 466} 467} 468let input :unknown = options . input , stream = false ; 469if ( input !== undefined ) { 470if ( typeof input !== "string" && tag ( input ) !== "Uint8Array" ) { 471try { 472if ( streamLocked ( input )) throw new TypeError ( "Input stream is locked" ); 473stream = true ; 474} catch { 475throw new TypeError ( 476"Input requires string, bytes, or an unlocked ReadableStream" , 477); 478} 479if ( sync ) throw new TypeError ( "runSync does not accept streaming input" ); 480} else input = bytes ( input , true ); 481} 482const terminalValue = options . terminal ; 483let terminal :NativeOptions [ "terminal" ] = null ; 484if ( terminalValue !== undefined ) { 485if ( capture || sync ) throw new TypeError ( "Terminals require spawn" ); 486const size = dictionary ( terminalValue ); 487keys ( size , [ "columns" , "rows" ]); 488terminal = { 489__proto__ :null , 490columns :integer ( size . columns , 80 , 1 , 65535 ), 491rows :integer ( size . rows , 24 , 1 , 65535 ), 492}; 493} 494const stdinValue = options . stdin , 495stdoutValue = options . stdout , 496stderrValue = options . stderr ; 497if ( 498input !== undefined && stdinValue !== undefined && stdinValue !== "piped" 499) throw new TypeError ( "input requires piped stdin" ); 500const stdin = stdio ( 501stdinValue , 502input !== undefined || terminal ?"piped" :"null" , 503true , 504), 505stdout = stdio ( stdoutValue , "piped" , false ), 506stderr = stdio ( stderrValue , terminal ?"stdout" :"piped" , false , true ); 507if ( capture && stdin . mode === "piped" && input === undefined ) { 508input = new Uint8Array ( 0 ); 509} 510const extra = dictionary ( options . extraStdio ), 511extraStdio = array <[ number , NativeStdio ]>(), 512descriptors = ReflectOwnKeys ( extra ); 513for ( let i = 0 ; i < descriptors . length ; i ++ ) { 514const key = descriptors [ i ]; 515if ( typeof key !== "string" ) { 516throw new TypeError ( "Extra stdio descriptor must be an integer" ); 517} 518const fd = integer ( Number ( key ), undefined , 3 , 2147483615 ) ! ; 519if ( String ( fd ) !== key ) { 520throw new TypeError ( "Noncanonical stdio descriptor" ); 521} 522const value = dictionary ( extra [ key ]); 523push ( extraStdio , args ( fd , stdio ( value , "piped" , false , false , true ))); 524} 525if (( capture || sync ) && extraStdio . length ) { 526throw new TypeError ( "Additional streams require spawn" ); 527} 528const termination = dictionary ( options . termination ); 529keys ( termination , [ "scope" , "signal" , "graceMs" ]); 530const scopeValue = termination . scope , signalValue = termination . signal ; 531const scope = scopeValue === undefined ?"group" :usv ( scopeValue ), 532signalName = signalValue === undefined ?"SIGTERM" :usv ( signalValue ), 533graceMs = integer ( termination . graceMs , 1000 ); 534if ( scope !== "group" && scope !== "child" ) { 535throw new TypeError ( "Invalid termination scope" ); 536} 537const timeoutMs = integer ( options . timeoutMs , null ), 538maxCollectedBytes = integer ( 539options . maxCollectedBytes , 540128 * 1024 * 1024 , 5411 , 542), 543maxBufferedBytes = integer ( options . maxBufferedBytes , 1024 * 1024 , 1 ), 544drainTimeoutMs = integer ( options . drainTimeoutMs , 1000 ); 545const check = boolean ( options . check , true ), encodingValue = options . encoding ; 546const encoding = encodingValue === undefined ?null :usv ( encodingValue ); 547const decoder = encoding === null ?null :new TextDecoder ( encoding ); 548const signal = options . signal ; 549if ( signal !== undefined ) { 550if ( sync ) throw new TypeError ( "runSync does not accept AbortSignal" ); 551const aborted = state ( signals , signal ); 552if ( aborted . aborted ) throw new ProcessAbortError ( null , aborted . reason ); 553} 554if ( stream && streamLocked ( input )) { 555throw new TypeError ( "Input stream is locked" ); 556} 557// run() always has a reader draining its output, collecting it or 558// streaming lines, so the child waits for that reader rather than racing 559// the unread limit. 560const backpressure = array < number >(); 561if ( capture && ! sync ) { 562if ( stdout . mode === "piped" ) push ( backpressure , 1 ); 563if ( stderr . mode === "piped" ) push ( backpressure , 2 ); 564} 565return { 566__proto__ :null , 567native :{ 568__proto__ :null , 569command :command as string , 570args :argv , 571 cwd, 572 searchPath, 573 clearEnv, 574 env, 575 stdin, 576 stdout, 577 stderr, 578 extraStdio, 579 terminal, 580 timeoutMs, 581maxCollectedBytes :sync ?maxCollectedBytes :null , 582 maxBufferedBytes, 583termination :{ __proto__ :null , scope, signal :signalName , graceMs}, 584 drainTimeoutMs, 585 backpressure, 586}, 587input :input as Prepared [ "input" ], 588 stream, 589 check, 590 decoder, 591decode :( bytes ) => decoder === null ?bytes :decode ( decoder , bytes ), 592 signal, 593maxCollectedBytes :maxCollectedBytes as number , 594}; 595} 596function keepAlive ( record :ProcessRecord , epoch :number ) :void { 597const pending = then ( op_process_keepalive ( args ( record . id , epoch )), result ); 598markPromiseHandled ( pending ); 599} 600function readable ( record :ProcessRecord , fd :number ) :ReadSource { 601const source :ReadSource = { 602__proto__ :null , 603owner :null , 604used :false , 605stream :null ! , 606}; 607source . stream = new ReadableStream < ProcessBytes >({ 608__proto__ :null , 609type :"bytes" , 610async pull ( controller ) { 611source . used = true ; 612if ( record . disposed ) { 613source . owner = null ; 614finish ( controller ); 615return ; 616} 617const chunk = result ( 618await safePromise ( op_process_read ( args ( record . id , fd ))), 619); 620if ( chunk === null ) { 621source . owner = null ; 622finish ( controller ); 623return ; 624} 625enqueue ( controller , chunk ); 626}, 627cancel () { 628source . used = true ; 629source . owner = null ; 630if ( ! record . disposed ) result ( op_process_cancel_read ( args ( record . id , fd ))); 631}, 632}, { __proto__ :null , highWaterMark :0 }); 633return source ; 634} 635function writable ( record :ProcessRecord , fd :number ) :WriteSource { 636const source :WriteSource = { __proto__ :null , owner :null , stream :null ! }; 637source . stream = new WritableStream < ProcessBytes >({ 638__proto__ :null , 639write ( value ) { 640if ( record . disposed ) { 641throw failure ( "BadResource" , "Subprocess is disposed" ); 642} 643return then ( op_process_write ( args ( record . id , fd , bytes ( value ))), result ); 644}, 645close () { 646source . owner = null ; 647if ( ! record . disposed ) { 648return then ( op_process_end ( args ( record . id , fd )), result ); 649} 650}, 651abort () { 652source . owner = null ; 653if ( ! record . disposed ) { 654return then ( op_process_end ( args ( record . id , fd )), result ); 655} 656}, 657}, { 658__proto__ :null , 659highWaterMark :65536 , 660size ( value ) { 661return tag ( value ) === "Uint8Array" ?byteLength ( value ) :1 ; 662}, 663}); 664return source ; 665} 666async function feed ( 667record :ProcessRecord , 668value :ProcessBytes | ReadableStream < ProcessBytes >, 669streaming :boolean , 670) :Promise < void > { 671const writer = writerOf ( record . writes [ 0 ] ! . stream ); 672let reader :ReadableStreamDefaultReader < ProcessBytes > | null = null , 673writing = false ; 674const cancellation = then ( record . completed , () => { 675if ( reader ) markPromiseHandled ( readerCancel ( reader )); 676}); 677markPromiseHandled ( cancellation ); 678try { 679if ( streaming ) reader = streamReader ( value as ReadableStream < ProcessBytes >); 680if ( reader ) { 681while ( true ) { 682writing = false ; 683const step = await safePromise ( read ( reader )); 684if ( step . done ) break ; 685writing = true ; 686await safePromise ( write ( writer , bytes ( step . value ))); 687} 688} else if ( byteLength ( value )) { 689writing = true ; 690await safePromise ( write ( writer , value )); 691} 692writing = true ; 693await safePromise ( closeWriter ( writer )); 694} catch ( error ) { 695if ( ! writing || errorCode ( error ) !== "BrokenPipe" ) { 696record . inputFailed = true ; 697record . inputError = error ; 698if ( ! record . finished && ! record . disposed ) { 699result ( op_process_stop ( args ( record . id , "io-error" ))); 700} 701} 702} finally { 703if ( reader ) { 704markPromiseHandled ( readerCancel ( reader )); 705releaseReader ( reader ); 706} 707releaseWriter ( writer ); 708} 709} 710async function collect ( 711source :ReadSource | undefined , 712budget :CollectionBudget , 713record :ProcessRecord , 714) :Promise < ProcessBytes | null > { 715if ( ! source ) return null ; 716const reader = streamReader ( source . stream ), chunks = array < ProcessBytes >(); 717let size = 0 ; 718try { 719while ( true ) { 720const step = await safePromise ( read ( reader )); 721if ( step . done ) break ; 722const count = byteLength ( step . value as ProcessBytes ), 723allowed = min ( count , budget . limit - budget . used ); 724if ( allowed ) { 725push ( 726chunks , 727allowed === count ?step . value as ProcessBytes :new Uint8Array ( 728buffer ( step . value as ProcessBytes ), 729byteOffset ( step . value as ProcessBytes ), 730allowed , 731), 732); 733size += allowed ; 734budget . used += allowed ; 735} 736if ( allowed < count && ! budget . exceeded ) { 737budget . exceeded = true ; 738if ( ! record . disposed ) { 739result ( op_process_stop ( args ( record . id , "output-limit" ))); 740} 741} 742} 743} finally { 744releaseReader ( reader ); 745} 746const bytes = new Uint8Array ( size ); 747let offset = 0 ; 748for ( let i = 0 ; i < chunks . length ; i ++ ) { 749setBytes ( bytes , chunks [ i ] ! , offset ); 750offset += byteLength ( chunks [ i ] ! ); 751} 752return bytes ; 753} 754function output < T > ( 755status :ProcessStatusValue , 756stdout :ProcessBytes | null , 757stderr :ProcessBytes | null , 758decodeOutput :( bytes :ProcessBytes ) => T , 759) :ProcessOutputValue < T > { 760return freeze ({ 761__proto__ :null , 762code :status . code , 763signal :status . signal , 764success :status . success , 765reason :status . reason , 766error :status . error , 767stdout :stdout === null ?null :decodeOutput ( stdout ), 768stderr :stderr === null ?null :decodeOutput ( stderr ), 769}); 770} 771function claim ( record :ProcessRecord ) :CollectionBudget { 772if ( record . collected ) { 773throw new TypeError ( "Subprocess output can only be collected once" ); 774} 775for ( let fd = 1 ; fd <= 2 ; fd ++ ) { 776const source = record . reads [ fd ]; 777if ( source && ( source . used || streamLocked ( source . stream ))) { 778throw new TypeError ( "Subprocess output was already consumed or locked" ); 779} 780} 781record . collected = true ; 782return { 783__proto__ :null , 784limit :record . collectLimit , 785used :0 , 786exceeded :false , 787}; 788} 789/** Settles a collection: the status reflects output and input failures. */ 790function settle ( 791record :ProcessRecord , 792budget :CollectionBudget , 793values :[ ProcessBytes | null , ProcessBytes | null , ProcessStatusValue , void ], 794) :Collection { 795const status :ProcessStatusValue & { __proto__ ?:null } = budget . exceeded 796 ?{ __proto__ :null , ...values [ 2 ], success :false , reason :"output-limit" } 797 :record . inputFailed && values [ 2 ]. reason === null 798 ?{ 799__proto__ :null , 800 ...values [ 2 ], 801success :false , 802reason :"io-error" , 803error :"Subprocess input failed" , 804} 805 :values [ 2 ]; 806return { 807__proto__ :null , 808 status, 809stdout :values [ 0 ], 810stderr :values [ 1 ], 811cause :status . reason === "abort" ?record . abortReason :record . inputError , 812}; 813} 814/** Collects raw output and status, unchecked. */ 815function gather ( 816child :ChildProcess , 817record :ProcessRecord , 818) :Promise < Collection > { 819const budget = claim ( record ); 820retain ( collecting , child ); 821const pending = then ( 822promiseAll ( 823[ 824collect ( record . reads [ 1 ], budget , record ), 825collect ( record . reads [ 2 ], budget , record ), 826record . completed , 827record . inputTask , 828] as const , 829), 830( values ) => settle ( record , budget , values ), 831); 832return then ( pending , ( value ) => { 833release ( collecting , child ); 834return value ; 835}, ( error ) => { 836release ( collecting , child ); 837throw error ; 838}); 839} 840/** 841* Collects like gather, but leaves stdout to lines(), which reads it as the 842* child writes. Settles independently of iteration. 843*/ 844function stream ( child :ChildProcess , record :ProcessRecord ) :LaunchedStream { 845const budget = claim ( record ); 846retain ( collecting , child ); 847const reader = streamReader ( record . reads [ 1 ] ! . stream ); 848const pending = then ( 849promiseAll ( 850[ 851null , 852collect ( record . reads [ 2 ], budget , record ), 853record . completed , 854record . inputTask , 855] as const , 856), 857( values ) => settle ( record , budget , values ), 858); 859const settled = then ( pending , ( value ) => { 860release ( collecting , child ); 861return value ; 862}, ( error ) => { 863release ( collecting , child ); 864throw error ; 865}); 866return { __proto__ :null , record, reader, settled}; 867} 868async function * streamLines ( launched :LaunchedStream ) :AsyncGenerator < string > { 869const record = launched . record , reader = launched . reader ; 870const decoder = new TextDecoder ( record . encoding ), 871options = { __proto__ :null , stream :true }; 872let pending = "" , finished = false , released = false , halted = false ; 873// A stopped run (timeout, abort, termination) ends the iteration without 874// the lines still queued behind the reader. 875const halt = () => { 876halted = true ; 877if ( ! released ) markPromiseHandled ( readerCancel ( reader )); 878}; 879markPromiseHandled ( then ( launched . settled , ( raw ) => { 880if ( raw . status . reason !== null ) halt (); 881}, halt )); 882const line = ( end :number ) => { 883const text = slice ( pending , 0 , end ); 884pending = slice ( pending , end + 1 ); 885return unterminated ( text ); 886}; 887try { 888 lines:while ( true ) { 889const step = await safePromise ( read ( reader )); 890if ( halted ) break ; 891if ( step . done ) { 892pending += decode ( decoder ); 893if ( pending !== "" ) yield line ( pending . length ); 894break ; 895} 896pending += decode ( decoder , step . value as ProcessBytes , options ); 897for ( let end ; ( end = indexOf ( pending , "\n" )) !== - 1 ;) { 898if ( halted ) break lines; 899yield line ( end ); 900} 901} 902finished = true ; 903} finally { 904if ( ! halted ) markPromiseHandled ( readerCancel ( reader )); 905if ( ! finished && ! record . finished && ! record . disposed ) { 906result ( op_process_stop ( args ( record . id , "terminated" ))); 907} 908released = true ; 909releaseReader ( reader ); 910} 911const raw = await safePromise ( launched . settled ); 912checked ( 913output ( raw . status , null , raw . stderr , record . decode ), 914record . check , 915raw . cause , 916); 917} 918function method < 919T extends object , 920K extends PropertyKey , 921V extends () => unknown , 922> ( 923object :T , 924name :K , 925value :V , 926) : assertsobject isT & Record < K , V > { 927define ( object , name , { 928__proto__ :null , 929 value, 930writable :true , 931configurable :true , 932} as PropertyDescriptor ); 933} 934function split ( text :string ) :string [] { 935const lines = array < string >(); 936let start = 0 ; 937for ( let end ; ( end = indexOf ( text , "\n" , start )) !== - 1 ; start = end + 1 ) { 938push ( lines , unterminated ( slice ( text , start , end ))); 939} 940if ( start < text . length ) push ( lines , unterminated ( slice ( text , start ))); 941return lines ; 942} 943function unterminated ( line :string ) :string { 944return line [ line . length - 1 ] === "\r" ?slice ( line , 0 , - 1 ) :line ; 945} 946/** 947* The run() promise, with methods reading stdout from the same result. 948* Collection starts on the next microtask unless lines() claims stdout 949* first, in which case it streams. 950*/ 951function invoke < T extends ProcessBytes | string > ( 952child :ChildProcess < T > | null , 953failed :boolean , 954failure :unknown , 955) :ProcessPromise < T > { 956const record = failed ?null :childRecord ( child ! ); 957let streamed :LaunchedStream | null = null ; 958const raw = then ( promiseResolve ( undefined ), () => { 959if ( failed ) throw failure ; 960return streamed ?streamed . settled :gather ( child ! , record ! ); 961}); 962const settled = then ( raw , ( raw ) => ({ 963__proto__ :null , 964output :checked ( 965output ( raw . status , raw . stdout , raw . stderr , record ! . decode ), 966record ! . check , 967raw . cause , 968), 969stdout :raw . stdout , 970})); 971markPromiseHandled ( settled ); 972const promise = then ( settled , ( value ) => value . output ); 973const stdout = () => { 974markPromiseHandled ( promise ); 975return then ( settled , ( value ) => { 976if ( value . stdout === null ) { 977throw new TypeError ( 978streamed 979 ?"Subprocess stdout was streamed by lines()" 980 :"Subprocess stdout is not piped" , 981); 982} 983return value . stdout ; 984}); 985}; 986const decodeText = ( bytes :ProcessBytes ) => 987decode ( record ! . decoder ?? utf8 , bytes ); 988method ( promise , "bytes" , function bytes () { 989return stdout (); 990}); 991method ( promise , "text" , function text () { 992return then ( stdout (), decodeText ); 993}); 994method ( promise , "json" , function json () { 995return then ( stdout (), ( bytes ) => parseJSON ( decodeText ( bytes ))); 996}); 997method ( promise , "lines" , function lines () { 998if ( failed ) { 999markPromiseHandled ( promise ); 1000return ( async function * () { 1001throw failure ; 1002})(); 1003} 1004if ( streamed ) throw new TypeError ( "Subprocess lines can only be read once" ); 1005if ( record ! . collected ) { 1006// Collection already began: split the buffered result instead. 1007const pending = stdout (); 1008markPromiseHandled ( pending ); 1009return ( async function * () { 1010const lines = split ( decodeText ( await safePromise ( pending ))); 1011for ( let i = 0 ; i < lines . length ; i ++ ) yield lines [ i ]; 1012})(); 1013} 1014if ( ! record ! . reads [ 1 ]) { 1015throw new TypeError ( "Subprocess stdout is not piped" ); 1016} 1017streamed = stream ( child ! , record ! ); 1018markPromiseHandled ( promise ); 1019return streamLines ( streamed ); 1020}); 1021return promise ; 1022} 1023declare const secretBrand :unique symbol ; 1024type Secret = { readonly [ secretBrand ] :true }; 1025// Keep the output type tied to its private record across the shared resource table. 1026let childRecord :< T extends ProcessBytes | string > ( 1027child :ChildProcess < T >, 1028) => ProcessRecord < T >; 1029class ChildProcess < T extends ProcessBytes | string = ProcessBytes | string > { 1030declare static readonly [ Symbol . hasInstance ] :( value :unknown ) => value isChildProcess ; 1031 #record:ProcessRecord < T >; 1032 1033static { 1034childRecord = ( child ) => { 1035state ( children , child ); 1036return child . #record; 1037}; 1038} 1039 1040constructor ( key :Secret , record :ProcessRecord < T >) { 1041if ( key !== secret ) throw new TypeError ( "Illegal constructor" ); 1042this . #record= record ; 1043set ( children , this , record ); 1044register ( finalizer , this , record . id , this ); 1045} 1046get pid () { 1047return state ( children , this ). pid ; 1048} 1049get stdin () { 1050const record = state ( children , this ); 1051return record . automaticInput ?null :record . writes [ 0 ]?. stream ?? null ; 1052} 1053get stdout () { 1054return state ( children , this ). reads [ 1 ]?. stream ?? null ; 1055} 1056get stderr () { 1057return state ( children , this ). reads [ 2 ]?. stream ?? null ; 1058} 1059get stdio () { 1060return state ( children , this ). stdio ; 1061} 1062get terminal () { 1063return state ( children , this ). terminal ; 1064} 1065get status () { 1066return state ( children , this ). status ; 1067} 1068get completed () { 1069return state ( children , this ). completed ; 1070} 1071output () :Promise < ProcessOutputValue < T >> { 1072const record = childRecord ( this ); 1073return then ( 1074gather ( this , record ), 1075( raw ) => 1076checked ( 1077output ( raw . status , raw . stdout , raw . stderr , record . decode ), 1078record . check , 1079raw . cause , 1080), 1081); 1082} 1083kill ( signal :string = "SIGTERM" ) :boolean { 1084const record = state ( children , this ); 1085return record . disposed 1086 ?false 1087 :result ( op_process_kill ( args ( record . id , usv ( signal )))); 1088} 1089terminate () :Promise < ProcessStatusValue > { 1090const record = state ( children , this ); 1091if ( ! record . disposed ) { 1092result ( op_process_stop ( args ( record . id , "terminated" ))); 1093} 1094return record . completed ; 1095} 1096ref () :void { 1097const record = state ( children , this ); 1098if ( ! record . disposed && ! record . referenced ) { 1099record . referenced = true ; 1100keepAlive ( record , result ( op_process_ref ( args ( record . id , true )))); 1101} 1102} 1103unref () :void { 1104const record = state ( children , this ); 1105if ( ! record . disposed && record . referenced ) { 1106record . referenced = false ; 1107result ( op_process_ref ( args ( record . id , false ))); 1108} 1109} 1110async [ asyncDispose ]() :Promise < void > { 1111const record = state ( children , this ); 1112if ( ! record . disposed ) { 1113result ( op_process_stop ( args ( record . id , "dispose" ))); 1114await safePromise ( record . completed ); 1115record . disposed = true ; 1116unregister ( finalizer , this ); 1117op_process_release ( record . id ); 1118} 1119} 1120} 1121define ( ChildProcess . prototype , SymbolToStringTag , { 1122value :"ChildProcess" , 1123configurable :true , 1124}); 1125function start < T extends ProcessBytes | string > ( 1126options :Prepared < T >, 1127) :ChildProcess < T > { 1128const info = result ( op_process_spawn ( options . native )); 1129const record :ProcessRecord < T > = { 1130__proto__ :null , 1131id :info . id , 1132pid :info . pid , 1133reads :ObjectCreate ( null ), 1134writes :ObjectCreate ( null ), 1135stdio :ObjectCreate ( null ), 1136terminal :null , 1137collectLimit :options . maxCollectedBytes , 1138decoder :options . decoder , 1139decode :options . decode , 1140encoding :options . decoder === null ?"utf-8" :encodingOf ( options . decoder ), 1141check :options . check , 1142automaticInput :options . input !== undefined , 1143collected :false , 1144disposed :false , 1145finished :false , 1146referenced :true , 1147inputTask :undefined , 1148inputFailed :false , 1149inputError :undefined , 1150abortReason :undefined , 1151status :undefined ! , 1152completed :undefined ! , 1153}; 1154const child = new ChildProcess ( secret , record ); 1155for ( let i = 0 ; i < info . readable . length ; i ++ ) { 1156const fd = info . readable [ i ]; 1157const source = readable ( record , fd ); 1158source . owner = child ; 1159record . reads [ fd ] = source ; 1160if ( fd >= 3 ) own ( record . stdio , fd , source . stream ); 1161} 1162for ( let i = 0 ; i < info . writable . length ; i ++ ) { 1163const fd = info . writable [ i ]; 1164const source = writable ( record , fd ); 1165source . owner = child ; 1166record . writes [ fd ] = source ; 1167if ( fd >= 3 ) own ( record . stdio , fd , source . stream ); 1168} 1169freeze ( record . stdio ); 1170record . status = then ( 1171op_process_wait ( args ( info . id , false )), 1172( reply ) => freeze ( result ( reply )), 1173); 1174record . completed = then ( 1175op_process_wait ( args ( info . id , true )), 1176( reply ) => freeze ( result ( reply )), 1177); 1178markPromiseHandled ( record . status ); 1179markPromiseHandled ( record . completed ); 1180keepAlive ( record , 0 ); 1181retain ( live , child ); 1182let removeAbort :(() => void ) | undefined ; 1183if ( options . signal !== undefined ) { 1184removeAbort = addAbort ( options . signal as AbortSignal , () => { 1185record . abortReason = state ( signals , options . signal ). reason ; 1186if ( ! record . finished && ! record . disposed ) { 1187result ( op_process_stop ( args ( record . id , "abort" ))); 1188} 1189}); 1190} 1191const cleanup = then ( record . completed , () => { 1192record . finished = true ; 1193removeAbort ?.(); 1194release ( live , child ); 1195}); 1196markPromiseHandled ( cleanup ); 1197if ( info . terminal ) { 1198record . terminal = freeze ({ 1199readable :record . reads [ 1 ] ! . stream , 1200writable :record . writes [ 0 ] ! . stream , 1201resize ( columns :number , rows :number ) { 1202required ( arguments . length , 2 ); 1203if ( record . disposed ) { 1204throw failure ( "BadResource" , "Subprocess is disposed" ); 1205} 1206result ( 1207op_process_resize ( 1208args ( 1209record . id , 1210integer ( columns , undefined , 1 , 65535 ) ! , 1211integer ( rows , undefined , 1 , 65535 ) ! , 1212), 1213), 1214); 1215}, 1216}); 1217} 1218if ( options . input !== undefined ) { 1219record . inputTask = feed ( record , options . input ! , options . stream ); 1220markPromiseHandled ( record . inputTask ); 1221} 1222return child ; 1223} 1224// Static methods retain the non-constructible function values while allowing overloads. 1225class Launch { 1226static spawn ( 1227command :string | URL , 1228options ?:SpawnOptions & { encoding ?:undefined }, 1229) :ChildProcess < ProcessBytes >; 1230static spawn ( 1231command :string | URL , 1232options :SpawnOptions & { encoding :string }, 1233) :ChildProcess < string >; 1234static spawn ( 1235command :string | URL , 1236args :Iterable < string > | undefined , 1237options ?:SpawnOptions & { args ?:undefined ; encoding ?:undefined }, 1238) :ChildProcess < ProcessBytes >; 1239static spawn ( 1240command :string | URL , 1241args :Iterable < string > | undefined , 1242options :SpawnOptions & { args ?:undefined ; encoding :string }, 1243) :ChildProcess < string >; 1244static spawn ( 1245command :string | URL , 1246second :unknown = undefined , 1247third :unknown = undefined , 1248) :ChildProcess { 1249required ( arguments . length , 1 ); 1250const call = invocation ( second , third ); 1251return start ( normalize ( command , call . args , call . options , false , false )); 1252} 1253static run ( 1254command :string | URL , 1255options ?:ProcessOptions & { encoding ?:undefined }, 1256) :ProcessPromise < ProcessBytes >; 1257static run ( 1258command :string | URL , 1259options :ProcessOptions & { encoding :string }, 1260) :ProcessPromise < string >; 1261static run ( 1262command :string | URL , 1263args :Iterable < string > | undefined , 1264options ?:ProcessOptions & { args ?:undefined ; encoding ?:undefined }, 1265) :ProcessPromise < ProcessBytes >; 1266static run ( 1267command :string | URL , 1268args :Iterable < string > | undefined , 1269options :ProcessOptions & { args ?:undefined ; encoding :string }, 1270) :ProcessPromise < string >; 1271static run ( 1272command :string | URL , 1273second :unknown = undefined , 1274third :unknown = undefined , 1275) :ProcessPromise { 1276let child :ChildProcess | null = null , failed = false , failure :unknown ; 1277try { 1278required ( arguments . length , 1 ); 1279const call = invocation ( second , third ); 1280child = start ( normalize ( command , call . args , call . options , true , false )); 1281} catch ( error ) { 1282failed = true ; 1283failure = error ; 1284} 1285return invoke ( child , failed , failure ); 1286} 1287static runSync ( 1288command :string | URL , 1289options ?:RunSyncOptions & { encoding ?:undefined }, 1290) :ProcessOutputValue < ProcessBytes >; 1291static runSync ( 1292command :string | URL , 1293options :RunSyncOptions & { encoding :string }, 1294) :ProcessOutputValue < string >; 1295static runSync ( 1296command :string | URL , 1297args :Iterable < string > | undefined , 1298options ?:RunSyncOptions & { args ?:undefined ; encoding ?:undefined }, 1299) :ProcessOutputValue < ProcessBytes >; 1300static runSync ( 1301command :string | URL , 1302args :Iterable < string > | undefined , 1303options :RunSyncOptions & { args ?:undefined ; encoding :string }, 1304) :ProcessOutputValue < string >; 1305static runSync ( 1306command :string | URL , 1307second :unknown = undefined , 1308third :unknown = undefined , 1309) :ProcessOutputValue { 1310required ( arguments . length , 1 ); 1311const call = invocation ( second , third ); 1312const prepared = normalize ( command , call . args , call . options , true , true ); 1313const reply = result ( 1314op_process_run_sync ( 1315args ( 1316prepared . native , 1317prepared . input === undefined ?null :prepared . input as ProcessBytes , 1318), 1319), 1320); 1321return checked ( 1322output ( reply . status , reply . stdout , reply . stderr , prepared . decode ), 1323prepared . check , 1324); 1325} 1326} 1327const { spawn, run, runSync} = Launch ; 1328export { 1329addSignalListener , 1330ChildProcess , 1331OutputLimitError , 1332ProcessAbortError , 1333ProcessError , 1334ProcessTimeoutError , 1335removeSignalListener , 1336run , 1337runSync , 1338spawn , 1339};