1+ const { spawn } = require ( 'child_process' ) ;
12const stream = require ( 'stream' ) ;
2- const Docker = require ( 'dockerode' ) ;
33const os = require ( 'os' ) ;
44const exitHook = require ( 'async-exit-hook' ) ;
55const fs = require ( 'mz/fs' ) ;
66
7-
87const logger = require ( './logger' ) ;
98
10-
119const studioCache = new Map ( ) ;
12- let hab , docker ;
10+ let hab ;
11+
12+ /**
13+ * Helper function to execute a Docker CLI command.
14+ * @param {Array<string> } args - The arguments to pass to the docker command.
15+ * @param {Object } options - Options for child_process.spawn.
16+ * @returns {Promise<string> } - Resolves with stdout data.
17+ */
18+ function execDocker ( args , options = { } ) {
19+ return new Promise ( ( resolve , reject ) => {
20+ const dockerProcess = spawn ( 'docker' , args , { stdio : 'pipe' , ...options } ) ;
21+
22+ let stdout = '' ;
23+ let stderr = '' ;
1324
25+ dockerProcess . stdout . on ( 'data' , ( data ) => {
26+ stdout += data . toString ( ) ;
27+ } ) ;
28+
29+ dockerProcess . stderr . on ( 'data' , ( data ) => {
30+ stderr += data . toString ( ) ;
31+ } ) ;
32+
33+ dockerProcess . on ( 'close' , ( code ) => {
34+ if ( code === 0 ) {
35+ resolve ( stdout . trim ( ) ) ;
36+ } else {
37+ reject ( new Error ( stderr . trim ( ) ) ) ;
38+ }
39+ } ) ;
40+ } ) ;
41+ }
1442
1543/**
1644 * A studio session that can be used to run multiple commands, using chroot if available or docker container
@@ -23,11 +51,15 @@ class Studio {
2351 for ( const [ gitDir , studio ] of studioCache ) {
2452 const { container } = studio ;
2553
26- if ( container && container . type != 'studio' ) {
54+ if ( container && container . type !== 'studio' ) {
2755 logger . info ( `terminating studio container: ${ container . id } ` ) ;
28- await container . stop ( ) ;
29- await container . remove ( ) ;
30- cleanupCount ++ ;
56+ try {
57+ await execDocker ( [ 'stop' , container . id ] ) ;
58+ await execDocker ( [ 'rm' , container . id ] ) ;
59+ cleanupCount ++ ;
60+ } catch ( err ) {
61+ logger . error ( `Failed to stop/remove container ${ container . id } : ${ err . message } ` ) ;
62+ }
3163 }
3264
3365 studioCache . delete ( gitDir ) ;
@@ -46,19 +78,6 @@ class Studio {
4678 return hab ;
4779 }
4880
49- static async getDocker ( ) {
50- if ( ! docker ) {
51- const { DOCKER_HOST : dockerHost } = process . env ;
52- const dockerHostMatch = dockerHost && dockerHost . match ( / ^ u n i x : \/ \/ ( \/ .* ) $ / ) ;
53- const socketPath = dockerHostMatch ? dockerHostMatch [ 1 ] : '/var/run/docker.sock' ;
54-
55- docker = new Docker ( { socketPath } ) ;
56- logger . info ( `connected to docker on: ${ socketPath } ` ) ;
57- }
58-
59- return docker ;
60- }
61-
6281 static async isEnvironmentStudio ( ) {
6382 return Boolean ( process . env . STUDIO_TYPE ) ;
6483 }
@@ -91,47 +110,17 @@ class Studio {
91110 }
92111
93112
94- // connect with Docker API
95- const docker = await Studio . getDocker ( ) ;
96-
97-
98113 // pull latest studio container
99114 try {
100- await new Promise ( ( resolve , reject ) => {
101- docker . pull ( 'jarvus/hologit-studio:latest' , ( streamErr , stream ) => {
102- if ( streamErr ) {
103- reject ( streamErr ) ;
104- return ;
105- }
106-
107- let lastStatus ;
108-
109- docker . modem . followProgress (
110- stream ,
111- ( err , output ) => {
112- if ( err ) {
113- reject ( err ) ;
114- } else {
115- resolve ( output ) ;
116- }
117- } ,
118- event => {
119- if ( event . status != lastStatus ) {
120- logger . info ( `docker pull: ${ event . status } ` ) ;
121- lastStatus = event . status ;
122- }
123- }
124- ) ;
125- } ) ;
126- } ) ;
115+ await execDocker ( [ 'pull' , 'jarvus/hologit-studio:latest' ] ) ;
127116 } catch ( err ) {
128117 logger . error ( `failed to pull studio image via docker: ${ err . message } ` ) ;
129118 }
130119
131120
132121 // find artifact cache
133122 const volumesConfig = { '/git' : { } } ;
134- const bindsConfig = [ `${ gitDir } :/git` ] ;
123+ const bindsConfig = [ `${ gitDir } :/git` ] ;
135124
136125 let artifactCachePath ;
137126
@@ -157,65 +146,69 @@ class Studio {
157146 }
158147
159148
160- // start studio container
161- let container ;
149+ // create studio container
150+ let containerId ;
151+ let defaultUser ;
162152 try {
163- container = await docker . createContainer ( {
164- Image : 'jarvus/hologit-studio' ,
165- Labels : {
166- 'sh.holo.studio' : 'yes'
167- } ,
168- AttachStdin : false ,
169- AttachStdout : true ,
170- AttachStderr : true ,
171- Env : [
172- 'STUDIO_TYPE=holo' ,
173- 'GIT_DIR=/git' ,
174- 'GIT_WORK_TREE=/hab/cache' ,
175- `DEBUG=${ process . env . DEBUG || '' } ` ,
176- `HAB_LICENSE=accept-no-persist`
177- ] ,
178- WorkingDir : '/git' ,
179- Volumes : volumesConfig ,
180- HostConfig : {
181- Binds : bindsConfig ,
182- // ExposedPorts: {
183- // "9229/tcp": { }
184- // },
185- // PortBindings: {
186- // '9229/tcp': [
187- // {
188- // HostIp: '0.0.0.0',
189- // HostPort: '9229'
190- // }
191- // ]
192- // }
193- }
194- } ) ;
153+ // Prepare environment variables
154+ const envArgs = [
155+ '--env' , 'STUDIO_TYPE=holo' ,
156+ '--env' , 'GIT_DIR=/git' ,
157+ '--env' , 'GIT_WORK_TREE=/hab/cache' ,
158+ '--env' , `DEBUG=${ process . env . DEBUG || '' } ` ,
159+ '--env' , 'HAB_LICENSE=accept-no-persist'
160+ ] ;
161+
162+ // Prepare volume bindings
163+ const volumeArgs = [ ] ;
164+ for ( const bind of bindsConfig ) {
165+ volumeArgs . push ( '-v' , bind ) ;
166+ }
167+
168+ // Create container
169+ const createArgs = [
170+ 'create' ,
171+ '--label' , 'sh.holo.studio=yes' ,
172+ '--workdir' , '/git' ,
173+ ...envArgs ,
174+ ...volumeArgs ,
175+ 'jarvus/hologit-studio:latest'
176+ ] ;
177+
178+ containerId = await execDocker ( createArgs ) ;
179+ containerId = containerId . split ( '\n' ) . pop ( ) . trim ( ) ; // Get the container ID from output
195180
196181 logger . info ( 'starting studio container' ) ;
197- await container . start ( ) ;
182+ await execDocker ( [ 'start' , containerId ] ) ;
198183
199184 const { uid, gid, username } = os . userInfo ( ) ;
200185
201186 if ( uid && gid && username ) {
202187 logger . info ( `configuring container to use user: ${ username } ` ) ;
203- await containerExec ( container , 'adduser' , '-u' , `${ uid } ` , '-G' , 'developer' , '-D' , username ) ;
204- await containerExec ( container , 'mkdir' , '-p' , `/home/${ username } /.hab` ) ;
205- await containerExec ( container , 'ln' , '-sf' , '/hab/cache' , `/home/${ username } /.hab/` ) ;
206- container . defaultUser = `${ uid } ` ;
188+ await containerExec ( { id : containerId } , 'adduser' , '-u' , `${ uid } ` , '-G' , 'developer' , '-D' , username ) ;
189+ await containerExec ( { id : containerId } , 'mkdir' , '-p' , `/home/${ username } /.hab` ) ;
190+ await containerExec ( { id : containerId } , 'ln' , '-sf' , '/hab/cache' , `/home/${ username } /.hab/` ) ;
191+ defaultUser = `${ uid } ` ;
207192 }
208193
209- const studio = new Studio ( { gitDir, container } ) ;
194+ const studio = new Studio ( { gitDir, container : { id : containerId , defaultUser } } ) ;
210195 studioCache . set ( gitDir , studio ) ;
211196 return studio ;
212197
213198 } catch ( err ) {
214- logger . error ( ' container failed: %o' , err ) ;
199+ logger . error ( ` container failed: ${ err . message } ` ) ;
215200
216- if ( container ) {
217- await container . stop ( ) ;
218- await container . remove ( ) ;
201+ if ( containerId ) {
202+ try {
203+ await execDocker ( [ 'stop' , containerId ] ) ;
204+ } catch ( stopErr ) {
205+ logger . error ( `Failed to stop container ${ containerId } : ${ stopErr . message } ` ) ;
206+ }
207+ try {
208+ await execDocker ( [ 'rm' , containerId ] ) ;
209+ } catch ( rmErr ) {
210+ logger . error ( `Failed to remove container ${ containerId } : ${ rmErr . message } ` ) ;
211+ }
219212 }
220213 }
221214 }
@@ -227,14 +220,14 @@ class Studio {
227220 }
228221
229222 isLocal ( ) {
230- return this . container . type == 'studio' ;
223+ return this . container . type === 'studio' ;
231224 }
232225
233226 /**
234227 * Run a command in the studio
235228 */
236229 async habExec ( ...command ) {
237- const options = typeof command [ command . length - 1 ] == 'object'
230+ const options = typeof command [ command . length - 1 ] === 'object'
238231 ? command . pop ( )
239232 : { } ;
240233
@@ -274,7 +267,7 @@ class Studio {
274267 // $env: { PATH }
275268 // }
276269 // );
277- if ( logger . level == 'debug' ) {
270+ if ( logger . level === 'debug' ) {
278271 command . unshift ( '--debug' ) ;
279272 }
280273
@@ -286,55 +279,66 @@ class Studio {
286279 }
287280
288281 async getPackage ( query , { install } = { install : false } ) {
289- let packagePath = await this . habExec ( 'pkg' , 'path' , query , { $nullOnError : true , $relayStderr : false } ) ;
282+ let packagePath ;
283+ try {
284+ packagePath = await this . habExec ( 'pkg' , 'path' , query , { $nullOnError : true , $relayStderr : false } ) ;
285+ } catch ( err ) {
286+ packagePath = null ;
287+ }
290288
291289 if ( ! packagePath && install ) {
292290 await this . habExec ( 'pkg' , 'install' , query ) ;
293- packagePath = await this . habExec ( 'pkg' , 'path' , query ) ;
291+ try {
292+ packagePath = await this . habExec ( 'pkg' , 'path' , query ) ;
293+ } catch ( err ) {
294+ packagePath = null ;
295+ }
294296 }
295297
296298 return packagePath ? packagePath . substr ( 10 ) : null ;
297299 }
298300}
299301
300-
301302exitHook ( callback => Studio . cleanup ( ) . then ( callback ) ) ;
302303
303-
304+ /**
305+ * Executes a command inside the specified Docker container.
306+ * @param {Object } container - The container object containing at least the `id` and optionally `defaultUser`.
307+ * @param {...string } command - The command and its arguments to execute.
308+ * @returns {Promise<string> } - Resolves with the command's stdout output.
309+ */
304310async function containerExec ( container , ...command ) {
305- const options = typeof command [ command . length - 1 ] == 'object'
311+ const options = typeof command [ command . length - 1 ] === 'object'
306312 ? command . pop ( )
307313 : { } ;
308314
309315 logger . info ( `studio-exec: ${ command . join ( ' ' ) } ` ) ;
310316
311- const env = [ ] ;
317+ const execArgs = [ 'exec' ] ;
318+
319+ if ( options . $user ) {
320+ execArgs . push ( '--user' , options . $user ) ;
321+ } else if ( container . defaultUser ) {
322+ execArgs . push ( '--user' , container . defaultUser ) ;
323+ }
324+
312325 if ( options . $env ) {
313- for ( const key of Object . keys ( options . $env ) ) {
314- env . push ( `${ key } =${ options . $env [ key ] } ` ) ;
326+ for ( const [ key , value ] of Object . entries ( options . $env ) ) {
327+ execArgs . push ( '--env' , `${ key } =${ value } ` ) ;
315328 }
316329 }
317330
318- const exec = await container . exec ( {
319- Cmd : command ,
320- AttachStdout : true ,
321- AttachStderr : options . $relayStderr !== false ,
322- Env : env ,
323- User : `${ container . defaultUser || options . $user || '' } `
324- } ) ;
325-
326- const execStream = await exec . start ( ) ;
327-
328- return new Promise ( ( resolve , reject ) => {
329- const output = [ ] ;
330- const outputStream = new stream . PassThrough ( ) ;
331-
332- outputStream . on ( 'data' , chunk => output . push ( chunk . toString ( 'utf8' ) ) ) ;
333- execStream . on ( 'end' , ( ) => resolve ( output . join ( '' ) . trim ( ) ) ) ;
331+ execArgs . push ( container . id , ...command ) ;
334332
335- container . modem . demuxStream ( execStream , outputStream , process . stderr ) ;
336- } ) ;
333+ try {
334+ const output = await execDocker ( execArgs ) ;
335+ return output ;
336+ } catch ( err ) {
337+ if ( options . $nullOnError ) {
338+ return null ;
339+ }
340+ throw err ;
341+ }
337342}
338343
339-
340344module . exports = Studio ;
0 commit comments