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');
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('-'));
}
}
});

View File

@ -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<void>((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<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) => {

View File

@ -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) {

View File

@ -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 {

View File

@ -18,6 +18,11 @@
<label>Dependencies</label> <span><%= build.dependencies %></span>
<label>Start time</label> <span class="to-local-time"><%= build.startTime %></span>
</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>
<div class="logs" id="logs">
<% (log || []).forEach(line => { %>