11import { Code } from 'mdast' ;
2+ import { createConnection } from 'net' ;
23import { join } from 'path' ;
34import { Option , assert } from 'ts-std' ;
45import { parseCommand } from '../../commands' ;
56import Options from '../../options' ;
67import parseArgs , { ToBool , optional } from '../../parse-args' ;
78import Servers from '../../servers' ;
89
10+ const DEFAULT_READY_TIMEOUT = 30000 ;
11+ const READY_RETRY_INTERVAL = 500 ;
12+
913interface Args {
1014 id ?: string ;
1115 lang ?: string ;
@@ -17,6 +21,68 @@ interface Args {
1721 captureOutput ?: boolean ;
1822}
1923
24+ function extractURL ( expect : Option < string > | undefined ) : Option < string > {
25+ if ( ! expect ) {
26+ return null ;
27+ }
28+
29+ let match = expect . match ( / h t t p s ? : \/ \/ [ ^ \s " ' ] + / ) ;
30+
31+ if ( match ) {
32+ try {
33+ return new URL ( match [ 0 ] ! ) . toString ( ) ;
34+ } catch {
35+ return null ;
36+ }
37+ } else {
38+ return null ;
39+ }
40+ }
41+
42+ async function waitForServerReady ( url : string , timeout : number ) : Promise < void > {
43+ let parsed = new URL ( url ) ;
44+ let host = parsed . hostname ;
45+ let port = parsed . port ? Number ( parsed . port ) : ( parsed . protocol === 'https:' ? 443 : 80 ) ;
46+ let startedAt = Date . now ( ) ;
47+ let lastError : Option < Error > = null ;
48+
49+ while ( Date . now ( ) - startedAt < timeout ) {
50+ try {
51+ await new Promise < void > ( ( resolve , reject ) => {
52+ let socket = createConnection ( { host, port } ) ;
53+
54+ socket . once ( 'connect' , ( ) => {
55+ socket . end ( ) ;
56+ resolve ( ) ;
57+ } ) ;
58+
59+ socket . once ( 'error' , e => {
60+ socket . destroy ( ) ;
61+
62+ if ( e instanceof Error ) {
63+ reject ( e ) ;
64+ } else {
65+ reject ( new Error ( String ( e ) ) ) ;
66+ }
67+ } ) ;
68+ } ) ;
69+
70+ return ;
71+ } catch ( e ) {
72+ if ( e instanceof Error ) {
73+ lastError = e ;
74+ } else {
75+ lastError = new Error ( String ( e ) ) ;
76+ }
77+ }
78+
79+ await new Promise ( resolve => setTimeout ( resolve , READY_RETRY_INTERVAL ) ) ;
80+ }
81+
82+ let details = lastError ? ` Last error: ${ lastError . message } ` : '' ;
83+ throw new Error ( `Timed out while waiting for ${ url } to accept connections after ${ timeout } ms.${ details } ` ) ;
84+ }
85+
2086export default async function startServer ( node : Code , options : Options , servers : Servers ) : Promise < Option < Code > > {
2187 let args = parseArgs < Args > ( node , [
2288 optional ( 'id' , String ) ,
@@ -72,7 +138,36 @@ export default async function startServer(node: Code, options: Options, servers:
72138 output . push ( `$ ${ display } ` ) ;
73139 }
74140
75- let stdout = await server . start ( args . expect ) ;
141+ let stdout = await server . start ( args . expect , args . timeout ) ;
142+
143+ let readyURL = extractURL ( args . expect ) ;
144+
145+ if ( readyURL ) {
146+ let timeout = args . timeout || DEFAULT_READY_TIMEOUT ;
147+
148+ try {
149+ await waitForServerReady ( readyURL , timeout ) ;
150+ } catch ( e ) {
151+ await server . kill ( ) ;
152+
153+ let message = ( e instanceof Error ) ? e . message : String ( e ) ;
154+
155+ throw new Error (
156+ `${ message }
157+
158+ ====== STDOUT ======
159+
160+ ${ server . stdout || '(No output)' }
161+
162+ ====== STDERR ======
163+
164+ ${ server . stderr || '(No output)' }
165+
166+ ====================
167+ `
168+ ) ;
169+ }
170+ }
76171
77172 if ( args . captureOutput && stdout ) {
78173 output . push ( stdout ) ;
0 commit comments