Add cancel button

This commit is contained in:
Cory Sanin 2025-01-14 03:23:59 -05:00
parent 40f04162d0
commit 413a756951
5 changed files with 70 additions and 20 deletions

View File

@ -7,6 +7,11 @@ document.addEventListener('DOMContentLoaded', function () {
const elements = document.getElementsByClassName('to-local-time'); const elements = document.getElementsByClassName('to-local-time');
for (let element of elements) { for (let element of elements) {
const innerText = element.innerText; const innerText = element.innerText;
if (element.firstChild) {
element.replaceChild(document.createTextNode(window.formatTime(innerText)), element.firstChild); element.replaceChild(document.createTextNode(window.formatTime(innerText)), element.firstChild);
} }
else {
element.appendChild(document.createTextNode('-'));
}
}
}); });

View File

@ -15,11 +15,15 @@ interface BuildEvent {
message: any; message: any;
} }
function getContainerName(id: number) {
return `archery-build-${id}`;
}
class BuildController extends EventEmitter { class BuildController extends EventEmitter {
private db: DB; private db: DB;
private process: spawn.ChildProcess | null = null;
private running: boolean = false; private running: boolean = false;
private interval: NodeJS.Timeout; private interval: NodeJS.Timeout;
private cancelled: boolean = false;
constructor(config = {}) { constructor(config = {}) {
super(); super();
@ -37,6 +41,7 @@ class BuildController extends EventEmitter {
private kickOffBuild = async () => { private kickOffBuild = async () => {
this.running = true; this.running = true;
this.cancelled = false;
const build = await this.db.dequeue(); const build = await this.db.dequeue();
if (build === null) { if (build === null) {
this.running = false; this.running = false;
@ -80,7 +85,7 @@ class BuildController extends EventEmitter {
private build = async (build: Build) => { private build = async (build: Build) => {
return new Promise<void>((resolve, reject) => { return new Promise<void>((resolve, reject) => {
const docker = this.process = spawn.spawn('docker', this.createBuildParams(build)); const docker = spawn.spawn('docker', this.createBuildParams(build));
docker.on('spawn', () => { docker.on('spawn', () => {
const remainder = { const remainder = {
std: '', std: '',
@ -106,8 +111,7 @@ class BuildController extends EventEmitter {
docker.stderr.on('data', createLogFunction('err')); docker.stderr.on('data', createLogFunction('err'));
}); });
docker.on('close', (code) => { docker.on('close', (code) => {
this.process = null; const status = code === 0 ? 'success' : (this.cancelled ? 'cancelled' : 'error');
const status = code === 0 ? 'success' : 'error';
this.emitLog({ this.emitLog({
id: build.id, id: build.id,
type: 'finish', type: 'finish',
@ -115,12 +119,7 @@ class BuildController extends EventEmitter {
}); });
this.db.finishBuild(build.id, status); this.db.finishBuild(build.id, status);
if (code === 0) {
resolve(); resolve();
}
else {
reject(code);
}
}); });
}); });
} }
@ -136,15 +135,40 @@ class BuildController extends EventEmitter {
// TODO: implement COMMIT // TODO: implement COMMIT
params.push('-e', `COMMIT=${build.commit}`); params.push('-e', `COMMIT=${build.commit}`);
} }
params.push('--name', getContainerName(build.id));
params.push(docker_images[build.distro]); params.push(docker_images[build.distro]);
return params; return params;
} }
cancelBuild = async (pid: number) => { cancelBuild = async (id: number) => {
const p = this.process const running = this.running;
if (p && p.pid === pid) { const build = await this.db.getBuild(id);
return p.kill(); if (running && build.status === 'queued') {
await this.db.finishBuild(id, 'cancelled');
return;
} }
await new Promise<void>((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) => { setDB = (db: DB) => {

View File

@ -19,7 +19,7 @@ function notStupidParseInt(v: string | undefined): number {
} }
function timeElapsed(date1: Date, date2: Date) { function timeElapsed(date1: Date, date2: Date) {
if (!date2 || ! date1) { if (!date2 || !date1) {
return '-'; return '-';
} }
const ms = Math.abs(date2.getTime() - date1.getTime()); const ms = Math.abs(date2.getTime() - date1.getTime());
@ -33,12 +33,12 @@ class Web {
private _webserver: http.Server | null = null; private _webserver: http.Server | null = null;
private db: DB; private db: DB;
private buildController: BuildController; private buildController: BuildController;
private app: Express; private app: expressWs.Application;
private port: number; private port: number;
constructor(options: WebConfig = {}) { constructor(options: WebConfig = {}) {
const app: Express = this.app = express(); const app: Express = express();
const wsApp = expressWs(app).app; const wsApp = this.app = expressWs(app).app;
this.port = notStupidParseInt(process.env.PORT) || options['port'] as number || 8080; this.port = notStupidParseInt(process.env.PORT) || options['port'] as number || 8080;
app.set('trust proxy', 1); 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) => { app.get('/build/:num/logs/?', async (req, res) => {
const build = await this.db.getBuild(parseInt(req.params.num)); const build = await this.db.getBuild(parseInt(req.params.num));
if (!build) { if (!build) {

View File

@ -99,6 +99,7 @@ form {
button, button,
a.button, a.button,
input[type="submit"] { input[type="submit"] {
font-size: 12pt;
background-color: var(--primary); background-color: var(--primary);
border: .4em double var(--primary); border: .4em double var(--primary);
border-radius: .6em; border-radius: .6em;
@ -106,7 +107,7 @@ input[type="submit"] {
padding: .3em .6em; padding: .3em .6em;
color: #fff; color: #fff;
margin-bottom: .4em; margin-bottom: .4em;
text-decoration: none;
box-shadow: 0px .4em 0 0 var(--primary-dark); box-shadow: 0px .4em 0 0 var(--primary-dark);
&:active { &:active {

View File

@ -18,6 +18,11 @@
<label>Dependencies</label> <span><%= build.dependencies %></span> <label>Dependencies</label> <span><%= build.dependencies %></span>
<label>Start time</label> <span class="to-local-time"><%= build.startTime %></span> <label>Start time</label> <span class="to-local-time"><%= build.startTime %></span>
</div> </div>
<% if (!ended) { %>
<div>
<a href="/build/<%= build.id %>/cancel" class="button">Cancel build</a>
</div>
<% } %>
<p><a href="/build/<%= build.id %>/logs">Full logs</a></p> <p><a href="/build/<%= build.id %>/logs">Full logs</a></p>
<div class="logs" id="logs"> <div class="logs" id="logs">
<% (log || []).forEach(line => { %> <% (log || []).forEach(line => { %>