import AssignmentFile from '../../../shared/types/Dojo/AssignmentFile'; import { TypedEmitter } from 'tiny-typed-emitter'; import ExerciseRunningEvents from '../../types/Dojo/ExerciseRunningEvents'; import { spawn } from 'child_process'; import ExerciseCheckerError from '../../../shared/types/Dojo/ExerciseCheckerError'; import { ChildProcessWithoutNullStreams } from 'node:child_process'; class ExerciseDockerCompose { readonly events: TypedEmitter<ExerciseRunningEvents> = new TypedEmitter<ExerciseRunningEvents>(); public displayableLogs: string = ''; public allLogs: string = ''; public isFinished: boolean = false; public success: boolean = false; public exitCode: number = -1; constructor(private projectName: string, private assignmentFile: AssignmentFile, private executionFolder: string, private composeFileOverride: Array<string> = []) { this.events.on('logs', (log: string, _error: boolean, displayable: boolean) => { this.allLogs += log; this.displayableLogs += displayable ? log : ''; }); this.events.on('finished', (success: boolean, exitCode: number) => { this.isFinished = true; this.success = success; this.exitCode = exitCode; }); } private registerChildProcess(childProcess: ChildProcessWithoutNullStreams, resolve: (value: (number | PromiseLike<number>)) => void, reject: (reason?: unknown) => void) { childProcess.stdout.on('data', (data) => { this.events.emit('logs', data.toString(), false, false); }); childProcess.stderr.on('data', (data) => { this.events.emit('logs', data.toString(), true, false); }); childProcess.on('exit', (code) => { code !== null ? resolve(code) : reject(); }); } run(doDown: boolean = false) { (async () => { let containerExitCode: number = -1; const dockerComposeCommand = `docker compose --project-name ${ this.projectName } --progress plain --file docker-compose.yml ${ this.composeFileOverride.map((file) => `--file "${ file }"`).join(' ') }`; // Run the service { try { this.events.emit('step', 'COMPOSE_RUN', 'Running Docker Compose file'); containerExitCode = await new Promise<number>((resolve, reject) => { this.events.emit('logs', '####################################################### Docker Compose & Main Container Logs #######################################################\n', false, false); const dockerCompose = spawn(`${ dockerComposeCommand } run --build --rm ${ this.assignmentFile.result.container }`, { cwd : this.executionFolder, shell: true, env : { 'DOCKER_BUILDKIT' : '1', 'BUILDKIT_PROGRESS': 'plain', ...process.env } }); this.registerChildProcess(dockerCompose, resolve, reject); }); } catch ( error ) { this.events.emit('endStep', 'COMPOSE_RUN', `Error while running the docker compose file`, true); this.events.emit('finished', false, ExerciseCheckerError.DOCKER_COMPOSE_RUN_ERROR); return; } this.events.emit('endStep', 'COMPOSE_RUN', `Docker Compose file run successfully`, false); } // Get linked services logs { try { this.events.emit('step', 'COMPOSE_LOGS', 'Linked services logs acquisition'); await new Promise<number>((resolve, reject) => { this.events.emit('logs', '####################################################### Other Services Logs #######################################################\n', false, false); const dockerCompose = spawn(`${ dockerComposeCommand } logs --timestamps`, { cwd : this.executionFolder, shell: true }); this.registerChildProcess(dockerCompose, resolve, reject); }); } catch ( error ) { this.events.emit('endStep', 'COMPOSE_LOGS', `Error while getting the linked services logs`, true); this.events.emit('finished', false, ExerciseCheckerError.DOCKER_COMPOSE_LOGS_ERROR); return; } this.events.emit('endStep', 'COMPOSE_LOGS', `Linked services logs acquired`, false); } // Remove containers if asked { if ( doDown ) { try { this.events.emit('step', 'COMPOSE_DOWN', 'Stopping and removing containers'); await new Promise<number>((resolve, reject) => { this.events.emit('logs', '####################################################### Stop and remove containers #######################################################\n', false, false); const dockerCompose = spawn(`${ dockerComposeCommand } down --volumes --rmi`, { cwd : this.executionFolder, shell: true }); this.registerChildProcess(dockerCompose, resolve, reject); }); } catch ( error ) { this.events.emit('endStep', 'COMPOSE_DOWN', `Error stop and remove containers`, true); this.events.emit('finished', false, ExerciseCheckerError.DOCKER_COMPOSE_DOWN_ERROR); return; } this.events.emit('endStep', 'COMPOSE_DOWN', `Containers stopped and removed`, false); } } // Remove images if asked { if ( doDown ) { try { this.events.emit('step', 'COMPOSE_REMOVE_DANGLING', 'Removing dangling images'); await new Promise<number>((resolve, reject) => { this.events.emit('logs', '####################################################### Remove dangling images #######################################################\n', false, false); const dockerCompose = spawn(`docker image prune --force`, { cwd : this.executionFolder, shell: true }); this.registerChildProcess(dockerCompose, resolve, reject); }); } catch ( error ) { this.events.emit('endStep', 'COMPOSE_REMOVE_DANGLING', `Error while removing dangling images`, true); this.events.emit('finished', false, ExerciseCheckerError.DOCKER_COMPOSE_REMOVE_DANGLING_ERROR); return; } this.events.emit('endStep', 'COMPOSE_REMOVE_DANGLING', `Dangling images removed`, false); } } this.events.emit('finished', true, containerExitCode); })(); } } export default ExerciseDockerCompose;