@@ -6,11 +6,103 @@ import { checkAccess, checkExists } from "./fileCheckers";
66import Debug from "debug" ;
77import { setTimeout as timer } from "node:timers/promises" ;
88import util from "node:util" ;
9+ import type { DevLaunchSpec } from "@moonwall/types" ;
10+ import Docker from "dockerode" ;
11+ import invariant from "tiny-invariant" ;
912
1013const execAsync = util . promisify ( exec ) ;
1114const debug = Debug ( "global:localNode" ) ;
1215
13- export async function launchNode ( cmd : string , args : string [ ] , name : string ) {
16+ // TODO: Add multi-threading support
17+ async function launchDockerContainer (
18+ imageName : string ,
19+ args : string [ ] ,
20+ name : string ,
21+ dockerConfig ?: DevLaunchSpec [ "dockerConfig" ]
22+ ) {
23+ const docker = new Docker ( ) ;
24+ const port = args . find ( ( a ) => a . includes ( "port" ) ) ?. split ( "=" ) [ 1 ] ;
25+ debug ( `\x1b[36mStarting Docker container ${ imageName } on port ${ port } ...\x1b[0m` ) ;
26+
27+ const dirPath = path . join ( process . cwd ( ) , "tmp" , "node_logs" ) ;
28+ const logLocation = path . join ( dirPath , `${ name } _docker_${ Date . now ( ) } .log` ) ;
29+ const fsStream = fs . createWriteStream ( logLocation ) ;
30+ process . env . MOON_LOG_LOCATION = logLocation ;
31+
32+ const portBindings = dockerConfig ?. exposePorts ?. reduce < Record < string , { HostPort : string } [ ] > > (
33+ ( acc , { hostPort, internalPort } ) => {
34+ acc [ `${ internalPort } /tcp` ] = [ { HostPort : hostPort . toString ( ) } ] ;
35+ return acc ;
36+ } ,
37+ { }
38+ ) ;
39+
40+ const rpcPort = args . find ( ( a ) => a . includes ( "rpc-port" ) ) ?. split ( "=" ) [ 1 ] ;
41+ invariant ( rpcPort , "RPC port not found, this is a bug" ) ;
42+
43+ const containerOptions = {
44+ Image : imageName ,
45+ platform : "linux/amd64" ,
46+ Cmd : args ,
47+ name : dockerConfig ?. containerName || `moonwall_${ name } _${ Date . now ( ) } ` ,
48+ ExposedPorts : {
49+ ...Object . fromEntries (
50+ dockerConfig ?. exposePorts ?. map ( ( { internalPort } ) => [ `${ internalPort } /tcp` , { } ] ) || [ ]
51+ ) ,
52+ [ `${ rpcPort } /tcp` ] : { } ,
53+ } ,
54+ HostConfig : {
55+ PortBindings : {
56+ ...portBindings ,
57+ [ `${ rpcPort } /tcp` ] : [ { HostPort : rpcPort } ] ,
58+ } ,
59+ } ,
60+ Env : dockerConfig ?. runArgs ?. filter ( ( arg ) => arg . startsWith ( "env:" ) ) . map ( ( arg ) => arg . slice ( 4 ) ) ,
61+ } satisfies Docker . ContainerCreateOptions ;
62+
63+ try {
64+ await pullImage ( imageName , docker ) ;
65+
66+ const container = await docker . createContainer ( containerOptions ) ;
67+ await container . start ( ) ;
68+
69+ const containerInfo = await container . inspect ( ) ;
70+ if ( ! containerInfo . State . Running ) {
71+ const errorMessage = `Container failed to start: ${ containerInfo . State . Error } ` ;
72+ console . error ( errorMessage ) ;
73+ fs . appendFileSync ( logLocation , `${ errorMessage } \n` ) ;
74+ throw new Error ( errorMessage ) ;
75+ }
76+
77+ for ( let i = 0 ; i < 300 ; i ++ ) {
78+ if ( await checkWebSocketJSONRPC ( Number . parseInt ( rpcPort ) ) ) {
79+ break ;
80+ }
81+ await timer ( 100 ) ;
82+ }
83+
84+ return { runningNode : container , fsStream } ;
85+ } catch ( error : unknown ) {
86+ if ( error instanceof Error ) {
87+ console . error ( `Docker container launch failed: ${ error . message } ` ) ;
88+ fs . appendFileSync ( logLocation , `Docker launch error: ${ error . message } \n` ) ;
89+ }
90+ throw error ;
91+ }
92+ }
93+
94+ export async function launchNode ( options : {
95+ command : string ;
96+ args : string [ ] ;
97+ name : string ;
98+ launchSpec ?: DevLaunchSpec ;
99+ } ) {
100+ const { command : cmd , args, name, launchSpec : config } = options ;
101+
102+ if ( config ?. useDocker ) {
103+ return launchDockerContainer ( cmd , args , name , config . dockerConfig ) ;
104+ }
105+
14106 if ( cmd . includes ( "moonbeam" ) ) {
15107 await checkExists ( cmd ) ;
16108 checkAccess ( cmd ) ;
@@ -193,3 +285,19 @@ async function findPortsByPid(pid: number, retryCount = 600, retryDelay = 100):
193285
194286 return [ ] ;
195287}
288+
289+ async function pullImage ( imageName : string , docker : Docker ) {
290+ console . log ( `Pulling Docker image: ${ imageName } ` ) ;
291+
292+ const pullStream = await docker . pull ( imageName ) ;
293+ // Dockerode pull doesn't wait for completion by default 🫠
294+ await new Promise ( ( resolve , reject ) => {
295+ docker . modem . followProgress ( pullStream , ( err : Error | null , output : any [ ] ) => {
296+ if ( err ) {
297+ reject ( err ) ;
298+ } else {
299+ resolve ( output ) ;
300+ }
301+ } ) ;
302+ } ) ;
303+ }
0 commit comments