diff --git a/scripts/timezone.js b/scripts/timezone.js index 369ea9e..1568c3d 100644 --- a/scripts/timezone.js +++ b/scripts/timezone.js @@ -7,6 +7,11 @@ document.addEventListener('DOMContentLoaded', function () { const elements = document.getElementsByClassName('to-local-time'); for (let element of elements) { const innerText = element.innerText; - element.replaceChild(document.createTextNode(window.formatTime(innerText)), element.firstChild); + if (element.firstChild) { + element.replaceChild(document.createTextNode(window.formatTime(innerText)), element.firstChild); + } + else { + element.appendChild(document.createTextNode('-')); + } } }); \ No newline at end of file diff --git a/src/BuildController.ts b/src/BuildController.ts index a43f2f2..476b16c 100644 --- a/src/BuildController.ts +++ b/src/BuildController.ts @@ -15,11 +15,15 @@ interface BuildEvent { message: any; } +function getContainerName(id: number) { + return `archery-build-${id}`; +} + class BuildController extends EventEmitter { private db: DB; - private process: spawn.ChildProcess | null = null; private running: boolean = false; private interval: NodeJS.Timeout; + private cancelled: boolean = false; constructor(config = {}) { super(); @@ -37,6 +41,7 @@ class BuildController extends EventEmitter { private kickOffBuild = async () => { this.running = true; + this.cancelled = false; const build = await this.db.dequeue(); if (build === null) { this.running = false; @@ -80,7 +85,7 @@ class BuildController extends EventEmitter { private build = async (build: Build) => { return new Promise((resolve, reject) => { - const docker = this.process = spawn.spawn('docker', this.createBuildParams(build)); + const docker = spawn.spawn('docker', this.createBuildParams(build)); docker.on('spawn', () => { const remainder = { std: '', @@ -106,8 +111,7 @@ class BuildController extends EventEmitter { docker.stderr.on('data', createLogFunction('err')); }); docker.on('close', (code) => { - this.process = null; - const status = code === 0 ? 'success' : 'error'; + const status = code === 0 ? 'success' : (this.cancelled ? 'cancelled' : 'error'); this.emitLog({ id: build.id, type: 'finish', @@ -115,12 +119,7 @@ class BuildController extends EventEmitter { }); this.db.finishBuild(build.id, status); - if (code === 0) { - resolve(); - } - else { - reject(code); - } + resolve(); }); }); } @@ -136,15 +135,40 @@ class BuildController extends EventEmitter { // TODO: implement COMMIT params.push('-e', `COMMIT=${build.commit}`); } + params.push('--name', getContainerName(build.id)); params.push(docker_images[build.distro]); return params; } - cancelBuild = async (pid: number) => { - const p = this.process - if (p && p.pid === pid) { - return p.kill(); + cancelBuild = async (id: number) => { + const running = this.running; + const build = await this.db.getBuild(id); + if (running && build.status === 'queued') { + await this.db.finishBuild(id, 'cancelled'); + return; } + await new Promise((resolve, reject) => { + const dockerPs = spawn.spawn('docker', ['ps', '--filter', `name=${getContainerName(id)}`, '--format', '{{.ID}}']); + let output = ''; + dockerPs.on('spawn', () => { + dockerPs.stdout.on('data', (data: Buffer | string) => { + output += data.toString(); + }); + }); + dockerPs.on('close', (code) => { + if (code > 0) { + return reject('failed to get container id'); + } + this.cancelled = true; + const dockerKill = spawn.spawn('docker', ['stop', output.trim()]); + dockerKill.on('close', (code) => { + if (code > 0) { + return reject('failed to kill container'); + } + resolve(); + }); + }); + }); } setDB = (db: DB) => { diff --git a/src/Web.ts b/src/Web.ts index 02fec01..81c7093 100644 --- a/src/Web.ts +++ b/src/Web.ts @@ -19,7 +19,7 @@ function notStupidParseInt(v: string | undefined): number { } function timeElapsed(date1: Date, date2: Date) { - if (!date2 || ! date1) { + if (!date2 || !date1) { return '-'; } const ms = Math.abs(date2.getTime() - date1.getTime()); @@ -33,12 +33,12 @@ class Web { private _webserver: http.Server | null = null; private db: DB; private buildController: BuildController; - private app: Express; + private app: expressWs.Application; private port: number; constructor(options: WebConfig = {}) { - const app: Express = this.app = express(); - const wsApp = expressWs(app).app; + const app: Express = express(); + const wsApp = this.app = expressWs(app).app; this.port = notStupidParseInt(process.env.PORT) || options['port'] as number || 8080; app.set('trust proxy', 1); @@ -120,6 +120,21 @@ class Web { }); }); + app.get('/build/:num/cancel', async (req, res) => { + const build = await this.db.getBuild(parseInt(req.params.num)); + if (!build) { + res.sendStatus(404); + return; + } + try { + await this.buildController.cancelBuild(build.id); + } + catch (ex) { + console.error(ex); + } + res.redirect(`/build/${build.id}`); + }); + app.get('/build/:num/logs/?', async (req, res) => { const build = await this.db.getBuild(parseInt(req.params.num)); if (!build) { diff --git a/styles/01-styles.scss b/styles/01-styles.scss index 9c7ad0b..f73cbfb 100644 --- a/styles/01-styles.scss +++ b/styles/01-styles.scss @@ -99,6 +99,7 @@ form { button, a.button, input[type="submit"] { + font-size: 12pt; background-color: var(--primary); border: .4em double var(--primary); border-radius: .6em; @@ -106,7 +107,7 @@ input[type="submit"] { padding: .3em .6em; color: #fff; margin-bottom: .4em; - + text-decoration: none; box-shadow: 0px .4em 0 0 var(--primary-dark); &:active { diff --git a/views/build.ejs b/views/build.ejs index f349149..ed5c249 100644 --- a/views/build.ejs +++ b/views/build.ejs @@ -18,6 +18,11 @@ <%= build.dependencies %> <%= build.startTime %> + <% if (!ended) { %> +
+ Cancel build +
+ <% } %>

Full logs

<% (log || []).forEach(line => { %>