11import { Command , Option } from "@commander-js/extra-typings" ;
22import chalk from "chalk" ;
33import { execa } from "execa" ;
4+ import { Listr , ListrTask } from "listr2" ;
5+ import pRetry from "p-retry" ;
46import path from "path" ;
7+ import { getServiceHealth } from "../../base.js" ;
58import { RollupsCommandOpts } from "../rollups.js" ;
69
710const commaSeparatedList = ( value : string , _previous : string [ ] ) =>
811 value . split ( "," ) ;
912
10- const availableServices = [
11- "bundler" ,
12- "explorer" ,
13- "graphql" ,
14- // "otterscan",
15- "paymaster" ,
13+ type Service = {
14+ name : string ; // name of the service
15+ file : string ; // docker compose file name
16+ healthySemaphore ?: string ; // service to check if the service is healthy
17+ healthyTitle ?: string | ( ( port : number ) => string ) ; // title of the service when it is healthy
18+ waitTitle ?: string ; // title of the service when it is starting
19+ errorTitle ?: string ; // title of the service when it is not healthy
20+ } ;
21+
22+ const host = "http://127.0.0.1" ;
23+
24+ // services configuration
25+ const standardServices : Service [ ] = [
26+ {
27+ name : "anvil" ,
28+ file : "docker-compose-anvil.yaml" ,
29+ healthySemaphore : "anvil" ,
30+ healthyTitle : `Service ${ chalk . cyan ( "anvil" ) } ready at ${ chalk . cyan ( `${ host } :8545` ) } ` ,
31+ waitTitle : `Starting ${ chalk . cyan ( "anvil" ) } ...` ,
32+ errorTitle : `Service ${ chalk . red ( "anvil" ) } failed` ,
33+ } ,
34+ {
35+ name : "proxy" ,
36+ file : "docker-compose-proxy.yaml" ,
37+ } ,
38+ {
39+ name : "database" ,
40+ file : "docker-compose-database.yaml" ,
41+ } ,
42+ {
43+ name : "rollups-node" ,
44+ file : "docker-compose-node.yaml" ,
45+ healthySemaphore : "proxy" ,
46+ healthyTitle : ( port ) =>
47+ `Service ${ chalk . cyan ( "rollups-node" ) } ready at ${ chalk . cyan ( `${ host } :${ port } /inspect/<address>` ) } ` ,
48+ waitTitle : `Starting ${ chalk . cyan ( "rollups-node" ) } ...` ,
49+ errorTitle : `Service ${ chalk . red ( "rollups-node" ) } failed` ,
50+ } ,
1651] ;
1752
53+ const availableServices : Service [ ] = [
54+ {
55+ name : "bundler" ,
56+ file : "docker-compose-bundler.yaml" ,
57+ healthySemaphore : "proxy" ,
58+ healthyTitle : ( port ) =>
59+ `Service ${ chalk . cyan ( "bundler" ) } ready at ${ chalk . cyan ( `${ host } :${ port } /bundler/rpc` ) } ` ,
60+ waitTitle : `Starting ${ chalk . cyan ( "bundler" ) } ...` ,
61+ errorTitle : `Service ${ chalk . red ( "bundler" ) } failed` ,
62+ } ,
63+ {
64+ name : "espresso" ,
65+ file : "docker-compose-espresso.yaml" ,
66+ healthySemaphore : "proxy" ,
67+ healthyTitle : ( port ) =>
68+ `Service ${ chalk . cyan ( "espresso" ) } ready at ${ chalk . cyan ( `${ host } :${ port } /espresso` ) } ` ,
69+ waitTitle : `Starting ${ chalk . cyan ( "espresso" ) } ...` ,
70+ errorTitle : `Service ${ chalk . red ( "espresso" ) } failed` ,
71+ } ,
72+ {
73+ name : "explorer" ,
74+ file : "docker-compose-explorer.yaml" ,
75+ healthySemaphore : "proxy" ,
76+ healthyTitle : ( port ) =>
77+ `Service ${ chalk . cyan ( "explorer" ) } ready at ${ chalk . cyan ( `${ host } :${ port } /explorer` ) } ` ,
78+ waitTitle : `Starting ${ chalk . cyan ( "explorer" ) } ...` ,
79+ errorTitle : `Service ${ chalk . red ( "explorer" ) } failed` ,
80+ } ,
81+ {
82+ name : "graphql" ,
83+ file : "docker-compose-graphql.yaml" ,
84+ healthySemaphore : "proxy" ,
85+ healthyTitle : ( port ) =>
86+ `Service ${ chalk . cyan ( "graphql" ) } ready at ${ chalk . cyan ( `${ host } :${ port } /graphql` ) } ` ,
87+ waitTitle : `Starting ${ chalk . cyan ( "graphql" ) } ...` ,
88+ errorTitle : `Service ${ chalk . red ( "graphql" ) } failed` ,
89+ } ,
90+ {
91+ name : "paymaster" ,
92+ file : "docker-compose-paymaster.yaml" ,
93+ healthySemaphore : "proxy" ,
94+ healthyTitle : ( port ) =>
95+ `Service ${ chalk . cyan ( "paymaster" ) } ready at ${ chalk . cyan ( `${ host } :${ port } /paymaster` ) } ` ,
96+ waitTitle : `Starting ${ chalk . cyan ( "paymaster" ) } ...` ,
97+ errorTitle : `Service ${ chalk . red ( "paymaster" ) } failed` ,
98+ } ,
99+ ] ;
100+
101+ const serviceMonitorTask = ( options : {
102+ errorTitle ?: string ;
103+ healthyTitle ?: string ;
104+ projectName : string ;
105+ service : string ;
106+ waitTitle ?: string ;
107+ } ) : ListrTask => {
108+ const { errorTitle, healthyTitle, service, waitTitle } = options ;
109+
110+ return {
111+ task : async ( _ctx , task ) => {
112+ await pRetry (
113+ async ( ) => {
114+ const health = await getServiceHealth ( options ) ;
115+ if ( health !== "healthy" ) {
116+ throw new Error (
117+ errorTitle ??
118+ `Service ${ chalk . cyan ( service ) } is not healthy` ,
119+ ) ;
120+ }
121+ } ,
122+ { retries : 100 , minTimeout : 500 , factor : 1.1 } ,
123+ ) ;
124+ task . title =
125+ healthyTitle ?? `Service ${ chalk . cyan ( service ) } is ready` ;
126+ } ,
127+ title : waitTitle ?? `Starting ${ chalk . cyan ( service ) } ...` ,
128+ } ;
129+ } ;
130+
18131export const createStartCommand = ( ) => {
19132 return new Command < [ ] , { } , RollupsCommandOpts > ( "start" )
20133 . description ( "Start a local rollups node environment." )
@@ -54,7 +167,6 @@ export const createStartCommand = () => {
54167 [ ] ,
55168 )
56169 . option ( "-p, --port <number>" , "port to listen on" , parseInt , 8080 )
57- . option ( "-d, --detach" , "run in detached mode" , false )
58170 . option ( "--dry-run" , "show the docker compose configuration" , false )
59171 . option ( "-v, --verbose" , "verbose output" , false )
60172 . action ( async ( options , command ) => {
@@ -63,7 +175,6 @@ export const createStartCommand = () => {
63175 blockTime,
64176 cpus,
65177 defaultBlock,
66- detach,
67178 dryRun,
68179 memory,
69180 port,
@@ -77,24 +188,18 @@ export const createStartCommand = () => {
77188 ) ;
78189
79190 // setup the environment variable used in docker compose
80- const listenPort = port ;
81191 const env : NodeJS . ProcessEnv = {
82192 ANVIL_VERBOSITY : verbose ? "--steps-tracing" : "--silent" ,
83193 BLOCK_TIME : blockTime . toString ( ) ,
84194 CARTESI_BLOCKCHAIN_DEFAULT_BLOCK : defaultBlock ,
85195 CARTESI_LOG_LEVEL : verbose ? "info" : "error" ,
86196 CARTESI_BIN_PATH : binPath ,
87- CARTESI_LISTEN_PORT : listenPort . toString ( ) ,
197+ CARTESI_LISTEN_PORT : port . toString ( ) ,
88198 CARTESI_ROLLUPS_NODE_CPUS : cpus ?. toString ( ) ,
89199 CARTESI_ROLLUPS_NODE_MEMORY : memory ?. toString ( ) ,
90200 } ;
91201
92- const composeFiles = [
93- "docker-compose-anvil.yaml" ,
94- "docker-compose-proxy.yaml" ,
95- "docker-compose-database.yaml" ,
96- "docker-compose-node.yaml" ,
97- ] ;
202+ const composeFiles = standardServices . map ( ( { file } ) => file ) ;
98203
99204 // cpu and memory limits, mostly for testing and debuggingpurposes
100205 if ( cpus ) {
@@ -104,21 +209,16 @@ export const createStartCommand = () => {
104209 composeFiles . push ( "docker-compose-node-memory.yaml" ) ;
105210 }
106211
212+ // select subset of optional services
107213 const optionalServices =
108214 services . length === 1 && services [ 0 ] === "all"
109215 ? availableServices
110- : services ;
111-
112- // validate services and add to compose files
113- for ( const service of optionalServices ) {
114- if ( ! availableServices . includes ( service ) ) {
115- throw new Error (
116- `Service ${ chalk . cyan ( service ) } not available` ,
117- ) ;
118- } else {
119- composeFiles . push ( `docker-compose-${ service } .yaml` ) ;
120- }
121- }
216+ : availableServices . filter ( ( { name } ) =>
217+ services . includes ( name ) ,
218+ ) ;
219+
220+ // add to compose files list
221+ composeFiles . push ( ...optionalServices . map ( ( { file } ) => file ) ) ;
122222
123223 // create the "--file <file>" list
124224 const files = composeFiles
@@ -128,63 +228,48 @@ export const createStartCommand = () => {
128228 ] )
129229 . flat ( ) ;
130230
131- const compose_args = [
231+ const composeArgs = [
132232 "compose" ,
133233 ...files ,
134234 "--project-name" ,
135235 projectName ,
136236 ] ;
137237
138- const up_args = [ ] ;
238+ // run in detached mode (background)
239+ const upArgs = [ "--detach" ] ;
139240
140- if ( detach ) {
141- // run in detached mode (background)
142- // will need to check logs using docker
143- up_args . push ( "--detach" ) ;
241+ if ( dryRun ) {
242+ // show the docker compose configuration
243+ await execa ( "docker" , [ ...composeArgs , "config" ] , {
244+ env,
245+ stdio : "inherit" ,
246+ } ) ;
144247 } else {
145- if ( ! verbose ) {
146- // attach only to rollups-node and prompt
147- compose_args . push ( "--progress" , "quiet" ) ;
148- up_args . push ( "--attach" , "rollups-node" ) ;
149- }
150- }
151-
152- // XXX: need this handler, so SIGINT can still call the finally block below
153- process . on ( "SIGINT" , ( ) => { } ) ;
154-
155- try {
156- if ( dryRun ) {
157- // show the docker compose configuration
158- await execa ( "docker" , [ ...compose_args , "config" ] , {
159- env,
160- stdio : "inherit" ,
161- } ) ;
162- return ;
163- }
164-
165248 // run compose environment
166- await execa ( "docker" , [ ...compose_args , "up" , ...up_args ] , {
249+ const up = execa ( "docker" , [ ...composeArgs , "up" , ...upArgs ] , {
167250 env,
168- stdio : "inherit" ,
169251 } ) ;
170- } catch ( e : unknown ) {
171- // 130 is a graceful shutdown, so we can swallow it
172- if ( ( e as any ) . exitCode !== 130 ) {
173- throw e ;
174- }
175- } finally {
176- // if it's detached, exit silently, because it's running in the background
177- if ( ! detach ) {
178- // shut it down, including volumes
179- await execa (
180- "docker" ,
181- [ ...compose_args , "down" , "--volumes" ] ,
182- {
183- env,
184- stdio : "inherit" ,
185- } ,
186- ) ;
187- }
252+
253+ // create tasks to monitor services startup
254+ const monitorTasks = [ ...standardServices , ...optionalServices ]
255+ . filter ( ( { healthySemaphore } ) => ! ! healthySemaphore ) // only services with a healthy semaphore
256+ . map ( ( service ) => {
257+ const healthyTitle =
258+ typeof service . healthyTitle === "function"
259+ ? service . healthyTitle ( port )
260+ : service . healthyTitle ;
261+ return serviceMonitorTask ( {
262+ projectName,
263+ service : service . healthySemaphore ! ,
264+ errorTitle : service . errorTitle ,
265+ waitTitle : service . waitTitle ,
266+ healthyTitle,
267+ } ) ;
268+ } ) ;
269+
270+ const tasks = new Listr ( monitorTasks , { concurrent : true } ) ;
271+ await tasks . run ( ) ;
272+ await up ;
188273 }
189274 } ) ;
190275} ;
0 commit comments