From 1343a9d0f1e748936e094ccf9b4065fc567689ce Mon Sep 17 00:00:00 2001 From: Izak Lipnik Date: Mon, 30 Oct 2017 14:24:23 +0100 Subject: [PATCH] feat(debug): stop execution when enter debug mode --- src/api/docker.ts | 11 +- src/api/process-manager.ts | 334 +++++++++++------- src/api/process.ts | 6 + src/api/socket.ts | 14 + .../app-dashboard.component.html | 14 +- .../app-dashboard/app-dashboard.component.ts | 13 +- .../components/app-job/app-job.component.html | 12 +- .../components/app-job/app-job.component.ts | 15 +- .../app-terminal/app-terminal.component.ts | 31 +- src/app/styles/build-details.sass | 12 +- src/app/styles/content.sass | 26 ++ src/app/styles/dashboard.sass | 4 + tests/unit/040_socket.spec.ts | 37 ++ 13 files changed, 386 insertions(+), 143 deletions(-) diff --git a/src/api/docker.ts b/src/api/docker.ts index 36b4fb23d..55e7764c0 100644 --- a/src/api/docker.ts +++ b/src/api/docker.ts @@ -8,6 +8,7 @@ import { CommandType } from './config'; import { yellow, green, red } from 'chalk'; import { ProcessOutput } from './process'; import { Readable } from 'stream'; +import { processes } from './process-manager'; export const docker = new dockerode(); @@ -240,12 +241,20 @@ export function getContainersStats(): Observable { let data = JSON.parse(rawJson); if (data && data.precpu_stats.system_cpu_usage) { + const jobId = container.Names[0].split('_')[2] || -1; + const job = processes.find(p => p.job_id === Number(jobId)); + let debug = false; + if (job) { + debug = job.debug || false; + } + const stats = { id: container.Id, name: container.Names[0].substr(1) || '', cpu: getCpuData(data), network: getNetworkData(data), - memory: getMemory(data) + memory: getMemory(data), + debug: debug }; stream.destroy(); diff --git a/src/api/process-manager.ts b/src/api/process-manager.ts index 3a216ad6c..16a4669cc 100644 --- a/src/api/process-manager.ts +++ b/src/api/process-manager.ts @@ -48,6 +48,7 @@ export interface JobProcess { env?: string[]; job?: Observable; exposed_ports?: string; + debug?: boolean; } export interface JobProcessEvent { @@ -169,138 +170,14 @@ export function startJobProcess(proc: JobProcess): Observable<{}> { observer.complete(); } }, err => { - proc.status = 'errored'; - const time = new Date(); const msg: LogMessageType = { message: `[error]: ${err}`, type: 'error', notify: false }; - logger.next(msg); - - dbJob.getLastRunId(proc.job_id) - .then(runId => { - const data = { - id: runId, - end_time: time, - status: 'failed', - log: proc.log.join('') - }; - - return dbJobRuns.updateJobRun(data); - }) - .then(build => updateBuild({ id: proc.build_id, end_time: time })) - .then(id => updateBuildRun({ id: id, end_time: time })) - .then(() => getBuild(proc.build_id)) - .then(build => sendFailureStatus(build, build.id)) - .then(() => { - jobEvents.next({ - type: 'process', - build_id: proc.build_id, - data: 'build failed', - additionalData: time.getTime() - }); - - buildStatuses.next({ - status: 'errored', - build_id: proc.build_id, - job_id: proc.job_id - }); - }) - .then(() => { - jobEvents.next({ - type: 'process', - build_id: proc.build_id, - job_id: proc.job_id, - data: 'job failed', - additionalData: time.getTime() - }); - }) - .then(() => observer.complete()) - .catch(err => { - const msg: LogMessageType = { - message: `[error]: ${err}`, type: 'error', notify: false - }; - logger.next(msg); - observer.complete(); - }); + jobFailed(proc, msg) + .then(() => observer.complete()); }, () => { - proc.status = 'success'; - const time = new Date(); - dbJob.getLastRunId(proc.job_id) - .then(runId => { - const data = { - id: runId, - end_time: time, - status: 'success', - log: proc.log.join('') - }; - - return dbJobRuns.updateJobRun(data); - }) - .then(() => getBuildStatus(proc.build_id)) - .then(status => { - buildStatuses.next({status: status, build_id: proc.build_id, job_id: proc.job_id}); - - if (status === 'success') { - return updateBuild({ id: proc.build_id, end_time: time }) - .then(() => getLastRunId(proc.build_id)) - .then(id => updateBuildRun({ id: id, end_time: time} )) - .then(() => getBuild(proc.build_id)) - .then(build => sendSuccessStatus(build, build.id)) - .then(() => { - jobEvents.next({ - type: 'process', - build_id: proc.build_id, - data: 'build succeeded', - additionalData: time.getTime() - }); - }); - } else if (status === 'failed') { - return getBuild(proc.build_id) - .then(build => updateBuild({ id: proc.build_id, end_time: time })) - .then(() => getLastRunId(proc.build_id)) - .then(id => updateBuildRun({ id: id, end_time: time} )) - .then(() => getBuild(proc.build_id)) - .then(build => sendFailureStatus(build, build.id)) - .then(() => { - jobEvents.next({ - type: 'process', - build_id: proc.build_id, - data: 'build failed', - additionalData: time.getTime() - }); - }); - } else { - return Promise.resolve(); - } - }) - .then(() => { - jobEvents.next({ - type: 'process', - build_id: proc.build_id, - job_id: proc.job_id, - data: 'job succeded', - additionalData: time.getTime() - }); - observer.complete(); - }) - .catch(err => { - const msg: LogMessageType = { - message: `[error]: ${err}`, type: 'error', notify: false - }; - logger.next(msg); - - jobEvents.next({ - type: 'process', - build_id: proc.build_id, - job_id: proc.job_id, - data: 'job failed', - additionalData: time.getTime() - }); - - getLastRunId(proc.build_id).then(id => updateBuildRun({ id: id, end_time: time} )); - - observer.complete(); - }); + jobSucceded(proc) + .then(() => observer.complete()); }); }) .then(() => dbJob.getLastRunId(proc.job_id)) @@ -330,6 +207,24 @@ export function startJobProcess(proc: JobProcess): Observable<{}> { export function restartJob(jobId: number): Promise { const time = new Date(); let job = null; + let process = processes.find(p => p.job_id === Number(jobId)); + if (process && process.debug) { + process.debug = false; + jobEvents.next({ + type: 'process', + build_id: process.build_id, + job_id: jobId, + data: 'job failed', + additionalData: time.getTime() + }); + + jobEvents.next({ + type: 'debug', + build_id: process.build_id, + job_id: jobId, + data: 'false' + }); + } return stopJob(jobId) .then(() => dbJob.getLastRun(jobId)) @@ -364,6 +259,24 @@ export function restartJob(jobId: number): Promise { export function stopJob(jobId: number): Promise { const time = new Date(); + let process = processes.find(p => p.job_id === Number(jobId)); + if (process && process.debug) { + process.debug = false; + jobEvents.next({ + type: 'process', + build_id: process.build_id, + job_id: jobId, + data: 'job failed', + additionalData: time.getTime() + }); + + jobEvents.next({ + type: 'debug', + build_id: process.build_id, + job_id: jobId, + data: 'false' + }); + } return Promise.resolve() .then(() => dbJob.getJob(jobId)) @@ -467,6 +380,29 @@ export function stopJob(jobId: number): Promise { }); } +export function debugJob(jobId: number, debug: boolean): Promise { + return new Promise((resolve, reject) => { + const time = new Date(); + let process = processes.find(p => p.job_id === Number(jobId)); + process.debug = debug; + + if (debug) { + buildSub[jobId].unsubscribe(); + delete buildSub[jobId]; + + const msg: JobProcessEvent = { + build_id: process.build_id, + job_id: Number(jobId), + type: 'data', + data: `[exectime]: stopped` + }; + + terminalEvents.next(msg); + process.log.push(`[exectime]: stopped`); + } + }); +} + export function startBuild(data: any, buildConfig?: any): Promise { let config: JobsAndEnv[]; let repoId = data.repositories_id; @@ -673,7 +609,8 @@ function queueJob(jobId: number): Promise { env: data.env, image_name: data.image, exposed_ports: null, - log: [] + log: [], + debug: false }; jobProcesses.next(jobProcess); @@ -685,3 +622,142 @@ function queueJob(jobId: number): Promise { }); }); } + +function jobSucceded(proc: JobProcess): Promise { + return Promise.resolve() + .then(() => { + proc.status = 'success'; + const time = new Date(); + return dbJob.getLastRunId(proc.job_id) + .then(runId => { + const data = { + id: runId, + end_time: time, + status: 'success', + log: proc.log.join('') + }; + + return dbJobRuns.updateJobRun(data); + }) + .then(() => getBuildStatus(proc.build_id)) + .then(status => { + buildStatuses.next({status: status, build_id: proc.build_id, job_id: proc.job_id}); + + if (status === 'success') { + return updateBuild({ id: proc.build_id, end_time: time }) + .then(() => getLastRunId(proc.build_id)) + .then(id => updateBuildRun({ id: id, end_time: time} )) + .then(() => getBuild(proc.build_id)) + .then(build => sendSuccessStatus(build, build.id)) + .then(() => { + jobEvents.next({ + type: 'process', + build_id: proc.build_id, + data: 'build succeeded', + additionalData: time.getTime() + }); + }); + } else if (status === 'failed') { + return getBuild(proc.build_id) + .then(build => updateBuild({ id: proc.build_id, end_time: time })) + .then(() => getLastRunId(proc.build_id)) + .then(id => updateBuildRun({ id: id, end_time: time} )) + .then(() => getBuild(proc.build_id)) + .then(build => sendFailureStatus(build, build.id)) + .then(() => { + jobEvents.next({ + type: 'process', + build_id: proc.build_id, + data: 'build failed', + additionalData: time.getTime() + }); + }); + } else { + return Promise.resolve(); + } + }) + .then(() => { + jobEvents.next({ + type: 'process', + build_id: proc.build_id, + job_id: proc.job_id, + data: 'job succeded', + additionalData: time.getTime() + }); + }) + .catch(err => { + const msg: LogMessageType = { + message: `[error]: ${err}`, type: 'error', notify: false + }; + logger.next(msg); + + jobEvents.next({ + type: 'process', + build_id: proc.build_id, + job_id: proc.job_id, + data: 'job failed', + additionalData: time.getTime() + }); + + getLastRunId(proc.build_id).then(id => updateBuildRun({ id: id, end_time: time} )); + Promise.resolve(); + }); + }); +} + +function jobFailed(proc: JobProcess, msg?: LogMessageType): Promise { + return Promise.resolve() + .then(() => { + proc.status = 'errored'; + const time = new Date(); + if (msg) { + logger.next(msg); + } + + return dbJob.getLastRunId(proc.job_id) + .then(runId => { + const data = { + id: runId, + end_time: time, + status: 'failed', + log: proc.log.join('') + }; + + return dbJobRuns.updateJobRun(data); + }) + .then(build => updateBuild({ id: proc.build_id, end_time: time })) + .then(id => updateBuildRun({ id: id, end_time: time })) + .then(() => getBuild(proc.build_id)) + .then(build => sendFailureStatus(build, build.id)) + .then(() => { + jobEvents.next({ + type: 'process', + build_id: proc.build_id, + data: 'build failed', + additionalData: time.getTime() + }); + + buildStatuses.next({ + status: 'errored', + build_id: proc.build_id, + job_id: proc.job_id + }); + }) + .then(() => { + jobEvents.next({ + type: 'process', + build_id: proc.build_id, + job_id: proc.job_id, + data: 'job failed', + additionalData: time.getTime() + }); + }) + .catch(err => { + const msg: LogMessageType = { + message: `[error]: ${err}`, type: 'error', notify: false + }; + logger.next(msg); + Promise.resolve(); + }); + }); +} diff --git a/src/api/process.ts b/src/api/process.ts index f875b89ad..3bc70ea61 100644 --- a/src/api/process.ts +++ b/src/api/process.ts @@ -154,6 +154,12 @@ export function startBuildProcess( }) .catch(err => console.error(err)); }); + + return () => { + if (sub) { + sub.unsubscribe(); + } + }; }); } diff --git a/src/api/socket.ts b/src/api/socket.ts index 1f146adf0..3f55eeb68 100644 --- a/src/api/socket.ts +++ b/src/api/socket.ts @@ -9,6 +9,7 @@ import { jobEvents, restartJob, stopJob, + debugJob, restartBuild, stopBuild, terminalEvents @@ -217,6 +218,18 @@ export class SocketServer { } break; + case 'debugJob': + if (this.clients[clientIndex].username === 'anonymous') { + conn.next({ type: 'error', data: 'not authorized' }); + } else { + conn.next({ type: 'request_received' }); + debugJob(event.data.jobId, event.data.debug) + .then(() => { + conn.next({ type: 'job debug', data: event.data.jobId }); + }); + } + break; + case 'subscribeToJobOutput': const jobId = parseInt(event.data.jobId, 10); const idx = processes.findIndex(proc => proc.job_id === jobId); @@ -224,6 +237,7 @@ export class SocketServer { const proc = processes[idx]; conn.next({ type: 'data', data: proc.log.join('\n') }); conn.next({ type: 'exposed ports', data: proc.exposed_ports || null }); + conn.next({ type: 'debug', data: proc.debug || null }); } const index = this.clients.findIndex(client => client.connection === conn); diff --git a/src/app/components/app-dashboard/app-dashboard.component.html b/src/app/components/app-dashboard/app-dashboard.component.html index 33b4cd6af..aa7253397 100644 --- a/src/app/components/app-dashboard/app-dashboard.component.html +++ b/src/app/components/app-dashboard/app-dashboard.component.html @@ -44,7 +44,19 @@

Container resource consumption

- {{ c.name }} +
+
+ {{ c.name }} +
+
+ Debugging +
+
+ + + +
+
diff --git a/src/app/components/app-dashboard/app-dashboard.component.ts b/src/app/components/app-dashboard/app-dashboard.component.ts index 75ded8966..165a62d17 100644 --- a/src/app/components/app-dashboard/app-dashboard.component.ts +++ b/src/app/components/app-dashboard/app-dashboard.component.ts @@ -1,5 +1,6 @@ import { Component, OnInit, OnDestroy } from '@angular/core'; import { StatsService } from '../../services/stats.service'; +import { SocketService } from '../../services/socket.service'; import { Subscription } from 'rxjs/Subscription'; import { schemeCategory20b } from 'd3'; @@ -18,7 +19,7 @@ export class AppDashboardComponent implements OnInit, OnDestroy { cpuCores: number[]; containers: any[]; - constructor(private statsService: StatsService) { + constructor(private statsService: StatsService, private socketService: SocketService) { this.loading = true; this.colors = schemeCategory20b; @@ -66,4 +67,14 @@ export class AppDashboardComponent implements OnInit, OnDestroy { this.statsService.stop(); } + + stopJob(e: MouseEvent, container: string): void { + e.preventDefault(); + e.stopPropagation(); + + const job = container.split('_'); + if (job.length === 3) { + this.socketService.emit({ type: 'stopJob', data: { jobId: job[2] } }); + } + } } diff --git a/src/app/components/app-job/app-job.component.html b/src/app/components/app-job/app-job.component.html index 747d3a05d..74f201b4d 100644 --- a/src/app/components/app-job/app-job.component.html +++ b/src/app/components/app-job/app-job.component.html @@ -27,7 +27,7 @@

- + @@ -149,7 +149,7 @@

{{ commitMessage }}

- SSH and VNC Daemon is enabled in this container. + SSH and VNC Daemon is enabled in this container. SSH: ssh {{ sshd.split(':')[0] }} -p {{ sshd.split(':')[1] }} -l root @@ -162,6 +162,14 @@

{{ commitMessage }}

password: abstrusePass
+
+ +
diff --git a/src/app/components/app-job/app-job.component.ts b/src/app/components/app-job/app-job.component.ts index ac59715e0..68e7c6968 100644 --- a/src/app/components/app-job/app-job.component.ts +++ b/src/app/components/app-job/app-job.component.ts @@ -42,6 +42,7 @@ export class AppJobComponent implements OnInit, OnDestroy { commitMessage: string; dateTime: string; dateTimeToNow: string; + debug = false; constructor( private socketService: SocketService, @@ -88,6 +89,9 @@ export class AppJobComponent implements OnInit, OnDestroy { const port = portData['5900/tcp'][0].HostPort; this.vnc = `${document.location.hostname}:${port}`; } + } else if (event.type === 'debug') { + const debug = event.data || 'false'; + this.debug = debug === 'true'; } }); @@ -95,7 +99,7 @@ export class AppJobComponent implements OnInit, OnDestroy { this.sub = this.socketService.outputEvents .filter(event => event.type === 'process') - .filter(event => event.job_id === parseInt(this.id, 10)) + .filter(event => parseInt(event.job_id, 10) === parseInt(this.id, 10)) .subscribe(event => { if (!this.jobRun) { return; @@ -187,6 +191,7 @@ export class AppJobComponent implements OnInit, OnDestroy { this.processing = true; this.sshd = null; this.vnc = null; + this.debug = false; this.socketService.emit({ type: 'restartJob', data: { jobId: this.id } }); } @@ -196,9 +201,17 @@ export class AppJobComponent implements OnInit, OnDestroy { this.processing = true; this.sshd = null; this.vnc = null; + this.debug = false; this.socketService.emit({ type: 'stopJob', data: { jobId: this.id } }); } + debugMode(e: MouseEvent): void { + e.preventDefault(); + e.stopPropagation(); + this.debug = !this.debug; + this.socketService.emit({ type: 'debugJob', data: { jobId: this.id, debug: this.debug } }); + } + terminalOutput(e: any): void { if (e === 'ready') { this.terminalReady = true; diff --git a/src/app/components/app-terminal/app-terminal.component.ts b/src/app/components/app-terminal/app-terminal.component.ts index 14f972762..be965cad8 100644 --- a/src/app/components/app-terminal/app-terminal.component.ts +++ b/src/app/components/app-terminal/app-terminal.component.ts @@ -91,12 +91,14 @@ export class AppTerminalComponent implements OnInit { re = new RegExp('(' + c + ')' + '[\\s\\S]+'); } let time = times[i] ? Number(times[i]) : null; - let out = output.match(re) && output.match(re)[2] ? output.match(re)[2].trim() : ''; out = out.replace(retime, ''); out = out.replace(/(\[success\]: .*)/igm, '$1'); out = out.replace(/(\[error\]: .*)/igm, '$1'); + if (output.includes('[exectime]: stopped')) { + out = out.replace('stopped', ''); + } return acc.concat({ command: curr.replace('==>', '').trim(), @@ -107,12 +109,14 @@ export class AppTerminalComponent implements OnInit { }, this.commands); } else { if (output.includes('[exectime]')) { - let retime = new RegExp('\\[exectime\]: \\d*', 'igm'); - let match = output.match(retime); - let time = Number((Number(match[0].replace('[exectime]: ', '')) / 10).toFixed(0)); - - if (this.commands[this.commands.length - 1]) { - this.commands[this.commands.length - 1].time = time ? this.getDuration(time) : '0ms'; + if (output !== '[exectime]: stopped') { + let retime = new RegExp('\\[exectime\]: \\d*', 'igm'); + let match = output.match(retime); + let time = Number((Number(match[0].replace('[exectime]: ', '')) / 10).toFixed(0)); + + if (this.commands[this.commands.length - 1]) { + this.commands[this.commands.length - 1].time = time ? this.getDuration(time) : '0ms'; + } } } else { output = output.replace(/(\[success\]: .*)/igm, '$1'); @@ -124,6 +128,19 @@ export class AppTerminalComponent implements OnInit { } } + if (output.includes('[exectime]: stopped')) { + if (this.commands[this.commands.length - 1]) { + this.commands[this.commands.length - 1].time = 'stopped'; + } + + this.commands.push({ + command: 'Execution stopped, entered in debug mode.', + visible: true, + output: '', + time: '...' + }); + } + if (this.commands && this.commands.length) { this.commands = this.commands.map((cmd, i) => { const v = i === this.commands.length - 1 || cmd.visible; diff --git a/src/app/styles/build-details.sass b/src/app/styles/build-details.sass index 5a9105320..04eba8ac3 100644 --- a/src/app/styles/build-details.sass +++ b/src/app/styles/build-details.sass @@ -106,6 +106,13 @@ .button margin-left: 10px +.span-debug + font-family: $font-family-semibold + color: $color-secondary !important + +.span-ssh + color: $text !important + .ssh-container display: flex align-items: center @@ -116,7 +123,10 @@ span margin: 0 10px 0 0 - color: $text + + .debug + margin-left: 30px + margin-right: 5px .icon margin-left: 10px diff --git a/src/app/styles/content.sass b/src/app/styles/content.sass index a62f6696e..45a233411 100644 --- a/src/app/styles/content.sass +++ b/src/app/styles/content.sass @@ -454,6 +454,32 @@ progress background: #C2CAD4 color: $white + &.yellow + background: #ffd43b + +.data-label-mini + border-radius: 5px + font-size: 10px + color: $white !important + padding: 5px 20px + width: 80px + margin: 0 auto + text-align: center + font-weight: $weight-semibold + + &.green + background: #36AF47 + + &.red + background: #ED1C24 + + &.bright + background: #C2CAD4 + color: $white + + &.yellow + background: #ffd43b + .text-time display: block color: $grey-light !important diff --git a/src/app/styles/dashboard.sass b/src/app/styles/dashboard.sass index f7fc1174f..6649c0a5a 100644 --- a/src/app/styles/dashboard.sass +++ b/src/app/styles/dashboard.sass @@ -190,3 +190,7 @@ &.is-selected background: $green !important + +.ion-power + margin-right: 4px !important + margin-top: 4px diff --git a/tests/unit/040_socket.spec.ts b/tests/unit/040_socket.spec.ts index 1ff34d1d2..a8f782ceb 100644 --- a/tests/unit/040_socket.spec.ts +++ b/tests/unit/040_socket.spec.ts @@ -264,6 +264,43 @@ describe('Socket Security', () => { .catch(err => console.error(err)); }); + it(`should not have permissions to trigger 'debugJob' event`, (done) => { + socket = new ws('ws://localhost:6501', null); + + socket.on('message', (data: any) => { + data = JSON.parse(data); + + if (data.type === 'error') { + expect(data.data).to.equal('not authorized'); + done(); + } + }); + + socket.on('open', () => socket.send(JSON.stringify({type: 'debugJob', data: null }))); + }); + + it(`should have permissions to trigger 'debugJob' event`, (done) => { + let loginData = { email: 'test@gmail.com', password: 'test' }; + let data = { jobId: 1, debug: false }; + + sendRequest(loginData, 'api/user/login') + .then((jwt: any) => { + socket = new ws('ws://localhost:6501', jwt.data); + + socket.on('message', (data: any) => { + data = JSON.parse(data); + if (data.type === 'request_received') { + done(); + } + }); + + socket.on('open', () => { + return socket.send(JSON.stringify({ type: 'subscribeToNotifications', data: data })); + }); + }) + .catch(err => console.error(err)); + }); + it(`should not have permissions to trigger 'subscribeToLogs' event`, (done) => { socket = new ws('ws://localhost:6501', null);