diff --git a/NodeApp/src/commander/assignment/AssignmentCommand.ts b/NodeApp/src/commander/assignment/AssignmentCommand.ts index 70f09e2ff18e17774067e71a3d6e773dbf201f18..90ae6167deaece3339766cc3f8a7b090263d2e79 100644 --- a/NodeApp/src/commander/assignment/AssignmentCommand.ts +++ b/NodeApp/src/commander/assignment/AssignmentCommand.ts @@ -3,6 +3,7 @@ import AssignmentCreateCommand from './subcommands/AssignmentCreateCommand'; import AssignmentPublishCommand from './subcommands/AssignmentPublishCommand'; import AssignmentUnpublishCommand from './subcommands/AssignmentUnpublishCommand'; import AssignmentCheckCommand from './subcommands/AssignmentCheckCommand'; +import AssignmentRunCommand from './subcommands/AssignmentRunCommand'; class AssignmentCommand extends CommanderCommand { @@ -16,6 +17,7 @@ class AssignmentCommand extends CommanderCommand { protected defineSubCommands() { AssignmentCreateCommand.registerOnCommand(this.command); AssignmentCheckCommand.registerOnCommand(this.command); + AssignmentRunCommand.registerOnCommand(this.command); AssignmentPublishCommand.registerOnCommand(this.command); AssignmentUnpublishCommand.registerOnCommand(this.command); } diff --git a/NodeApp/src/commander/assignment/subcommands/AssignmentRunCommand.ts b/NodeApp/src/commander/assignment/subcommands/AssignmentRunCommand.ts new file mode 100644 index 0000000000000000000000000000000000000000..e33c755cf8416eec60414a582e68bcc436899fd7 --- /dev/null +++ b/NodeApp/src/commander/assignment/subcommands/AssignmentRunCommand.ts @@ -0,0 +1,24 @@ +import CommanderCommand from '../../CommanderCommand'; +import Config from '../../../config/Config'; +import ExerciseRunHelper from '../../../helpers/Dojo/ExerciseRunHelper'; + + +class AssignmentRunCommand extends CommanderCommand { + protected commandName: string = 'run'; + + protected defineCommand() { + // This command is synced with the "exercise run" command + this.command + .description('locally run the assignment as an exercise') + .option('-p, --path <value>', 'exercise path', Config.folders.defaultLocalExercise) + .option('-v, --verbose', 'verbose mode (display docker compose logs in live)') + .action(this.commandAction.bind(this)); + } + + protected async commandAction(options: { path: string, verbose: boolean }): Promise<void> { + await ExerciseRunHelper.run(options); + } +} + + +export default new AssignmentRunCommand(); \ No newline at end of file diff --git a/NodeApp/src/commander/exercise/subcommands/ExerciseRunCommand.ts b/NodeApp/src/commander/exercise/subcommands/ExerciseRunCommand.ts index cc139fe0a399909a51bee3a976d2d8debf2d5fce..90282b7955b1dccfdb20925e4f20fd8b3337c9a4 100644 --- a/NodeApp/src/commander/exercise/subcommands/ExerciseRunCommand.ts +++ b/NodeApp/src/commander/exercise/subcommands/ExerciseRunCommand.ts @@ -1,38 +1,13 @@ -import CommanderCommand from '../../CommanderCommand'; -import Config from '../../../config/Config'; -import fs from 'node:fs'; -import ora from 'ora'; -import util from 'util'; -import { exec } from 'child_process'; -import chalk from 'chalk'; -import * as os from 'os'; -import path from 'path'; -import ClientsSharedConfig from '../../../sharedByClients/config/ClientsSharedConfig'; -import AssignmentFile from '../../../shared/types/Dojo/AssignmentFile'; -import ExerciseDockerCompose from '../../../sharedByClients/helpers/Dojo/ExerciseDockerCompose'; -import SharedAssignmentHelper from '../../../shared/helpers/Dojo/SharedAssignmentHelper'; -import ExerciseCheckerError from '../../../shared/types/Dojo/ExerciseCheckerError'; -import ClientsSharedExerciseHelper from '../../../sharedByClients/helpers/Dojo/ClientsSharedExerciseHelper'; -import ExerciseResultsSanitizerAndValidator from '../../../sharedByClients/helpers/Dojo/ExerciseResultsSanitizerAndValidator'; - - -const execAsync = util.promisify(exec); +import CommanderCommand from '../../CommanderCommand'; +import Config from '../../../config/Config'; +import ExerciseRunHelper from '../../../helpers/Dojo/ExerciseRunHelper'; class ExerciseRunCommand extends CommanderCommand { protected commandName: string = 'run'; - private readonly dateISOString: string = (new Date()).toISOString().replace(/:/g, '_').replace(/\./g, '_'); - - private readonly folderResultsVolume: string = path.join(os.homedir(), 'DojoExecutions', `dojo_execLogs_${ this.dateISOString }`); - private readonly folderResultsDojo: string = path.join(this.folderResultsVolume, `Dojo/`); - private readonly folderResultsExercise: string = path.join(this.folderResultsVolume, `Exercise/`); - - private readonly projectName: string = `${ ClientsSharedConfig.dockerCompose.projectName }_${ this.dateISOString.toLowerCase() }`; - - private readonly fileComposeLogs: string = path.join(this.folderResultsDojo, `dockerComposeLogs.txt`); - protected defineCommand() { + // This command is synced with the "assignment run" command this.command .description('locally run an exercise') .option('-p, --path <value>', 'exercise path', Config.folders.defaultLocalExercise) @@ -40,222 +15,8 @@ class ExerciseRunCommand extends CommanderCommand { .action(this.commandAction.bind(this)); } - private displayExecutionLogs() { - ora({ - text : `${ chalk.magenta('Execution logs folder:') } ${ this.folderResultsVolume }`, - indent: 0 - }).start().info(); - } - protected async commandAction(options: { path: string, verbose: boolean }): Promise<void> { - const localExercisePath: string = options.path ?? Config.folders.defaultLocalExercise; - - let assignmentFile: AssignmentFile; - let exerciseDockerCompose: ExerciseDockerCompose; - let exerciseResultsValidation: ExerciseResultsSanitizerAndValidator; - - let haveResultsVolume: boolean; - - // Step 1: Check requirements (if it's an exercise folder and if Docker daemon is running) - { - console.log(chalk.cyan('Please wait while we are checking and creating dependencies...')); - - // Create result temp folder - fs.mkdirSync(this.folderResultsVolume, { recursive: true }); - fs.mkdirSync(this.folderResultsDojo, { recursive: true }); - fs.mkdirSync(this.folderResultsExercise, { recursive: true }); - - - ora({ - text : `Checking exercise content:`, - indent: 4 - }).start().info(); - - // Exercise folder - { - const spinner: ora.Ora = ora({ - text : `Checking exercise folder`, - indent: 8 - }).start(); - - const files = fs.readdirSync(options.path); - const missingFiles = Config.exercise.neededFiles.map((file: string): [ string, boolean ] => [ file, files.includes(file) ]).filter((file: [ string, boolean ]) => !file[1]); - - if ( missingFiles.length > 0 ) { - spinner.fail(`The exercise folder is missing the following files: ${ missingFiles.map((file: [ string, boolean ]) => file[0]).join(', ') }`); - return; - } - - spinner.succeed(`The exercise folder contains all the needed files`); - } - - // dojo_assignment.json validity - { - const spinner: ora.Ora = ora({ - text : `Checking ${ ClientsSharedConfig.assignment.filename } file`, - indent: 8 - }).start(); - - const validationResults = SharedAssignmentHelper.validateDescriptionFile(path.join(options.path, ClientsSharedConfig.assignment.filename)); - if ( !validationResults.isValid ) { - spinner.fail(`The ${ ClientsSharedConfig.assignment.filename } file is invalid: ${ JSON.stringify(validationResults.errors) }`); - return; - } else { - assignmentFile = validationResults.results!; - } - - haveResultsVolume = assignmentFile.result.volume !== undefined; - - spinner.succeed(`The ${ ClientsSharedConfig.assignment.filename } file is valid`); - } - - // Docker daemon - { - const spinner: ora.Ora = ora({ - text : `Checking Docker daemon`, - indent: 4 - }).start(); - - try { - await execAsync(`cd "${ Config.folders.defaultLocalExercise }";docker ps`); - } catch ( error ) { - spinner.fail(`The Docker daemon is not running`); - return; - } - - spinner.succeed(`The Docker daemon is running`); - } - } - - - // Step 2: Run docker-compose file - { - console.log(chalk.cyan('Please wait while we are running the exercise...')); - - let composeFileOverride: string[] = []; - const composeOverridePath: string = path.join(localExercisePath, 'docker-compose-override.yml'); - if ( haveResultsVolume ) { - const composeOverride = fs.readFileSync(path.join(__dirname, '../../../../assets/docker-compose-override.yml'), 'utf8').replace('{{VOLUME_NAME}}', assignmentFile.result.volume!).replace('{{MOUNT_PATH}}', this.folderResultsExercise); - fs.writeFileSync(composeOverridePath, composeOverride); - - composeFileOverride = [ composeOverridePath ]; - } - - exerciseDockerCompose = new ExerciseDockerCompose(this.projectName, assignmentFile, localExercisePath, composeFileOverride); - - try { - await new Promise<void>((resolve, reject) => { - let spinner: ora.Ora; - - if ( options.verbose ) { - exerciseDockerCompose.events.on('logs', (log: string, _error: boolean, displayable: boolean) => { - if ( displayable ) { - console.log(log); - } - }); - } - - exerciseDockerCompose.events.on('step', (name: string, message: string) => { - spinner = ora({ - text : message, - indent: 4 - }).start(); - - if ( options.verbose && name == 'COMPOSE_RUN' ) { - spinner.info(); - } - }); - - exerciseDockerCompose.events.on('endStep', (stepName: string, message: string, error: boolean) => { - if ( error ) { - if ( options.verbose && stepName == 'COMPOSE_RUN' ) { - ora({ - text : message, - indent: 4 - }).start().fail(); - } else { - spinner.fail(message); - } - } else { - if ( options.verbose && stepName == 'COMPOSE_RUN' ) { - ora({ - text : message, - indent: 4 - }).start().succeed(); - } else { - spinner.succeed(message); - } - } - }); - - exerciseDockerCompose.events.on('finished', (success: boolean) => { - success ? resolve() : reject(); - }); - - exerciseDockerCompose.run(true); - }); - } catch ( error ) { /* empty */ } - - fs.rmSync(composeOverridePath, { force: true }); - fs.writeFileSync(this.fileComposeLogs, exerciseDockerCompose.allLogs); - - if ( !exerciseDockerCompose.success ) { - this.displayExecutionLogs(); - return; - } - } - - - // Step 3: Get results - { - console.log(chalk.cyan('Please wait while we are checking the results...')); - - exerciseResultsValidation = new ExerciseResultsSanitizerAndValidator(this.folderResultsDojo, this.folderResultsExercise, exerciseDockerCompose.exitCode); - - try { - await new Promise<void>((resolve, reject) => { - let spinner: ora.Ora; - - exerciseResultsValidation.events.on('step', (name: string, message: string) => { - spinner = ora({ - text : message, - indent: 4 - }).start(); - }); - - exerciseResultsValidation.events.on('endStep', (stepName: string, message: string, error: boolean) => { - if ( error ) { - if ( stepName == 'CHECK_SIZE' ) { - spinner.warn(message); - } else { - spinner.fail(message); - } - } else { - spinner.succeed(message); - } - }); - - exerciseResultsValidation.events.on('finished', (success: boolean, exitCode: number) => { - success || exitCode == ExerciseCheckerError.EXERCISE_RESULTS_FOLDER_TOO_BIG ? resolve() : reject(); - }); - - exerciseResultsValidation.run(); - }); - } catch ( error ) { - this.displayExecutionLogs(); - return; - } - } - - - // Step 4: Display results + Volume location - { - ClientsSharedExerciseHelper.displayExecutionResults(exerciseResultsValidation.exerciseResults!, exerciseDockerCompose.exitCode, { - INFO : chalk.bold, - SUCCESS: chalk.green, - FAILURE: chalk.red - }, `\n\n${ chalk.bold('Execution results folder') } : ${ this.folderResultsVolume }`); - } + await ExerciseRunHelper.run(options); } } diff --git a/NodeApp/src/helpers/Dojo/ExerciseRunHelper.ts b/NodeApp/src/helpers/Dojo/ExerciseRunHelper.ts new file mode 100644 index 0000000000000000000000000000000000000000..82c7651d57ea69245e024ba78c21656553a765c0 --- /dev/null +++ b/NodeApp/src/helpers/Dojo/ExerciseRunHelper.ts @@ -0,0 +1,252 @@ +import ora from 'ora'; +import chalk from 'chalk'; +import Config from '../../config/Config'; +import AssignmentFile from '../../shared/types/Dojo/AssignmentFile'; +import ExerciseDockerCompose from '../../sharedByClients/helpers/Dojo/ExerciseDockerCompose'; +import ExerciseResultsSanitizerAndValidator from '../../sharedByClients/helpers/Dojo/ExerciseResultsSanitizerAndValidator'; +import fs from 'node:fs'; +import ClientsSharedConfig from '../../sharedByClients/config/ClientsSharedConfig'; +import SharedAssignmentHelper from '../../shared/helpers/Dojo/SharedAssignmentHelper'; +import path from 'path'; +import ExerciseCheckerError from '../../shared/types/Dojo/ExerciseCheckerError'; +import ClientsSharedExerciseHelper from '../../sharedByClients/helpers/Dojo/ClientsSharedExerciseHelper'; +import os from 'os'; +import util from 'util'; +import { exec } from 'child_process'; + + +const execAsync = util.promisify(exec); + + +class ExerciseRunHelper { + private readonly dateISOString: string = (new Date()).toISOString().replace(/:/g, '_').replace(/\./g, '_'); + + private readonly folderResultsVolume: string = path.join(os.homedir(), 'DojoExecutions', `dojo_execLogs_${ this.dateISOString }`); + private readonly folderResultsDojo: string = path.join(this.folderResultsVolume, `Dojo/`); + private readonly folderResultsExercise: string = path.join(this.folderResultsVolume, `Exercise/`); + + private readonly projectName: string = `${ ClientsSharedConfig.dockerCompose.projectName }_${ this.dateISOString.toLowerCase() }`; + + private readonly fileComposeLogs: string = path.join(this.folderResultsDojo, `dockerComposeLogs.txt`); + + private displayExecutionLogs() { + ora({ + text : `${ chalk.magenta('Execution logs folder:') } ${ this.folderResultsVolume }`, + indent: 0 + }).start().info(); + } + + async run(options: { path: string, verbose: boolean }): Promise<void> { + const localExercisePath: string = options.path ?? Config.folders.defaultLocalExercise; + + let assignmentFile: AssignmentFile; + let exerciseDockerCompose: ExerciseDockerCompose; + let exerciseResultsValidation: ExerciseResultsSanitizerAndValidator; + + let haveResultsVolume: boolean; + + // Step 1: Check requirements (if it's an exercise folder and if Docker daemon is running) + { + console.log(chalk.cyan('Please wait while we are checking and creating dependencies...')); + + // Create result temp folder + fs.mkdirSync(this.folderResultsVolume, { recursive: true }); + fs.mkdirSync(this.folderResultsDojo, { recursive: true }); + fs.mkdirSync(this.folderResultsExercise, { recursive: true }); + + + ora({ + text : `Checking exercise content:`, + indent: 4 + }).start().info(); + + // Exercise folder + { + const spinner: ora.Ora = ora({ + text : `Checking exercise folder`, + indent: 8 + }).start(); + + const files = fs.readdirSync(options.path); + const missingFiles = Config.exercise.neededFiles.map((file: string): [ string, boolean ] => [ file, files.includes(file) ]).filter((file: [ string, boolean ]) => !file[1]); + + if ( missingFiles.length > 0 ) { + spinner.fail(`The exercise folder is missing the following files: ${ missingFiles.map((file: [ string, boolean ]) => file[0]).join(', ') }`); + return; + } + + spinner.succeed(`The exercise folder contains all the needed files`); + } + + // dojo_assignment.json validity + { + const spinner: ora.Ora = ora({ + text : `Checking ${ ClientsSharedConfig.assignment.filename } file`, + indent: 8 + }).start(); + + const validationResults = SharedAssignmentHelper.validateDescriptionFile(path.join(options.path, ClientsSharedConfig.assignment.filename)); + if ( !validationResults.isValid ) { + spinner.fail(`The ${ ClientsSharedConfig.assignment.filename } file is invalid: ${ JSON.stringify(validationResults.errors) }`); + return; + } else { + assignmentFile = validationResults.results!; + } + + haveResultsVolume = assignmentFile.result.volume !== undefined; + + spinner.succeed(`The ${ ClientsSharedConfig.assignment.filename } file is valid`); + } + + // Docker daemon + { + const spinner: ora.Ora = ora({ + text : `Checking Docker daemon`, + indent: 4 + }).start(); + + try { + await execAsync(`cd "${ Config.folders.defaultLocalExercise }";docker ps`); + } catch ( error ) { + spinner.fail(`The Docker daemon is not running`); + return; + } + + spinner.succeed(`The Docker daemon is running`); + } + } + + + // Step 2: Run docker-compose file + { + console.log(chalk.cyan('Please wait while we are running the exercise...')); + + let composeFileOverride: string[] = []; + const composeOverridePath: string = path.join(localExercisePath, 'docker-compose-override.yml'); + if ( haveResultsVolume ) { + const composeOverride = fs.readFileSync(path.join(__dirname, '../../../assets/docker-compose-override.yml'), 'utf8').replace('{{VOLUME_NAME}}', assignmentFile.result.volume!).replace('{{MOUNT_PATH}}', this.folderResultsExercise); + fs.writeFileSync(composeOverridePath, composeOverride); + + composeFileOverride = [ composeOverridePath ]; + } + + exerciseDockerCompose = new ExerciseDockerCompose(this.projectName, assignmentFile, localExercisePath, composeFileOverride); + + try { + await new Promise<void>((resolve, reject) => { + let spinner: ora.Ora; + + if ( options.verbose ) { + exerciseDockerCompose.events.on('logs', (log: string, _error: boolean, displayable: boolean) => { + if ( displayable ) { + console.log(log); + } + }); + } + + exerciseDockerCompose.events.on('step', (name: string, message: string) => { + spinner = ora({ + text : message, + indent: 4 + }).start(); + + if ( options.verbose && name == 'COMPOSE_RUN' ) { + spinner.info(); + } + }); + + exerciseDockerCompose.events.on('endStep', (stepName: string, message: string, error: boolean) => { + if ( error ) { + if ( options.verbose && stepName == 'COMPOSE_RUN' ) { + ora({ + text : message, + indent: 4 + }).start().fail(); + } else { + spinner.fail(message); + } + } else { + if ( options.verbose && stepName == 'COMPOSE_RUN' ) { + ora({ + text : message, + indent: 4 + }).start().succeed(); + } else { + spinner.succeed(message); + } + } + }); + + exerciseDockerCompose.events.on('finished', (success: boolean) => { + success ? resolve() : reject(); + }); + + exerciseDockerCompose.run(true); + }); + } catch ( error ) { /* empty */ } + + fs.rmSync(composeOverridePath, { force: true }); + fs.writeFileSync(this.fileComposeLogs, exerciseDockerCompose.allLogs); + + if ( !exerciseDockerCompose.success ) { + this.displayExecutionLogs(); + return; + } + } + + + // Step 3: Get results + { + console.log(chalk.cyan('Please wait while we are checking the results...')); + + exerciseResultsValidation = new ExerciseResultsSanitizerAndValidator(this.folderResultsDojo, this.folderResultsExercise, exerciseDockerCompose.exitCode); + + try { + await new Promise<void>((resolve, reject) => { + let spinner: ora.Ora; + + exerciseResultsValidation.events.on('step', (_name: string, message: string) => { + spinner = ora({ + text : message, + indent: 4 + }).start(); + }); + + exerciseResultsValidation.events.on('endStep', (stepName: string, message: string, error: boolean) => { + if ( error ) { + if ( stepName == 'CHECK_SIZE' ) { + spinner.warn(message); + } else { + spinner.fail(message); + } + } else { + spinner.succeed(message); + } + }); + + exerciseResultsValidation.events.on('finished', (success: boolean, exitCode: number) => { + success || exitCode == ExerciseCheckerError.EXERCISE_RESULTS_FOLDER_TOO_BIG ? resolve() : reject(); + }); + + exerciseResultsValidation.run(); + }); + } catch ( error ) { + this.displayExecutionLogs(); + return; + } + } + + + // Step 4: Display results + Volume location + { + ClientsSharedExerciseHelper.displayExecutionResults(exerciseResultsValidation.exerciseResults!, exerciseDockerCompose.exitCode, { + INFO : chalk.bold, + SUCCESS: chalk.green, + FAILURE: chalk.red + }, `\n\n${ chalk.bold('Execution results folder') } : ${ this.folderResultsVolume }`); + } + } +} + + +export default new ExerciseRunHelper(); \ No newline at end of file