web sockets and live logging

This commit is contained in:
Cory Sanin 2025-01-14 00:34:24 -05:00
parent 68005e8492
commit 16e62ff901
10 changed files with 185 additions and 10 deletions

1
.gitignore vendored
View File

@ -104,7 +104,6 @@ dist
.tern-port
# Custom
*.js
assets/css/
assets/js/
assets/webp/

60
package-lock.json generated
View File

@ -12,6 +12,7 @@
"body-parser": "^1.20.3",
"ejs": "3.1.10",
"express": "^4.21.2",
"express-ws": "^5.0.2",
"pg": "^8.13.1",
"pg-hstore": "^2.3.4",
"sequelize": "^6.37.5"
@ -19,6 +20,7 @@
"devDependencies": {
"@types/csso": "5.0.4",
"@types/express": "^5.0.0",
"@types/express-ws": "3.0.5",
"@types/node": "^22.10.5",
"@types/uglify-js": "3.17.5",
"csso": "5.0.5",
@ -412,6 +414,18 @@
"@types/send": "*"
}
},
"node_modules/@types/express-ws": {
"version": "3.0.5",
"resolved": "https://registry.npmjs.org/@types/express-ws/-/express-ws-3.0.5.tgz",
"integrity": "sha512-lbWMjoHrm/v85j81UCmb/GNZFO3genxRYBW1Ob7rjRI+zxUBR+4tcFuOpKKsYQ1LYTYiy3356epLeYi/5zxUwA==",
"dev": true,
"license": "MIT",
"dependencies": {
"@types/express": "*",
"@types/express-serve-static-core": "*",
"@types/ws": "*"
}
},
"node_modules/@types/http-errors": {
"version": "2.0.4",
"resolved": "https://registry.npmjs.org/@types/http-errors/-/http-errors-2.0.4.tgz",
@ -494,6 +508,16 @@
"integrity": "sha512-6SlHBzUW8Jhf3liqrGGXyTJSIFe4nqlJ5A5KaMZ2l/vbM3Wh3KSybots/wfWVzNLK4D1NZluDlSQIbIEPx6oyA==",
"license": "MIT"
},
"node_modules/@types/ws": {
"version": "8.5.13",
"resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.5.13.tgz",
"integrity": "sha512-osM/gWBTPKgHV8XkTunnegTRIsvF6owmf5w+JtAfOw472dptdm0dlGv4xCt6GwQRcC2XVOvvRE/0bAoQcL2QkA==",
"dev": true,
"license": "MIT",
"dependencies": {
"@types/node": "*"
}
},
"node_modules/accepts": {
"version": "1.3.8",
"resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz",
@ -930,6 +954,21 @@
"url": "https://opencollective.com/express"
}
},
"node_modules/express-ws": {
"version": "5.0.2",
"resolved": "https://registry.npmjs.org/express-ws/-/express-ws-5.0.2.tgz",
"integrity": "sha512-0uvmuk61O9HXgLhGl3QhNSEtRsQevtmbL94/eILaliEADZBHZOQUAiHFrGPrgsjikohyrmSG5g+sCfASTt0lkQ==",
"license": "BSD-2-Clause",
"dependencies": {
"ws": "^7.4.6"
},
"engines": {
"node": ">=4.5.0"
},
"peerDependencies": {
"express": "^4.0.0 || ^5.0.0-alpha.1"
}
},
"node_modules/filelist": {
"version": "1.0.4",
"resolved": "https://registry.npmjs.org/filelist/-/filelist-1.0.4.tgz",
@ -2102,6 +2141,27 @@
"@types/node": "*"
}
},
"node_modules/ws": {
"version": "7.5.10",
"resolved": "https://registry.npmjs.org/ws/-/ws-7.5.10.tgz",
"integrity": "sha512-+dbF1tHwZpXcbOJdVOkzLDxZP1ailvSxM6ZweXTegylPny803bFhA+vqBYw4s31NSAk4S2Qz+AKXK9a4wkdjcQ==",
"license": "MIT",
"engines": {
"node": ">=8.3.0"
},
"peerDependencies": {
"bufferutil": "^4.0.1",
"utf-8-validate": "^5.0.2"
},
"peerDependenciesMeta": {
"bufferutil": {
"optional": true
},
"utf-8-validate": {
"optional": true
}
}
},
"node_modules/xtend": {
"version": "4.0.2",
"resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz",

View File

@ -23,6 +23,7 @@
"body-parser": "^1.20.3",
"ejs": "3.1.10",
"express": "^4.21.2",
"express-ws": "^5.0.2",
"pg": "^8.13.1",
"pg-hstore": "^2.3.4",
"sequelize": "^6.37.5"
@ -30,6 +31,7 @@
"devDependencies": {
"@types/csso": "5.0.4",
"@types/express": "^5.0.0",
"@types/express-ws": "3.0.5",
"@types/node": "^22.10.5",
"@types/uglify-js": "3.17.5",
"csso": "5.0.5",

61
scripts/build.js Normal file
View File

@ -0,0 +1,61 @@
document.addEventListener('DOMContentLoaded', function () {
const logContainer = this.getElementById('logs');
const followLogsBtn = this.getElementById('followCheckmark');
const buildStatusTxt = this.getElementById('buildStatus');
/**
* Get the correct path to establish a ws connection
* @param {Location} loc
*/
function wsPath(loc) {
return loc.pathname.replace(/\/$/, '') + '/ws';
}
/**
* Add log line to the DOM
* @param {string} str
*/
function appendLine(str, e = false) {
const p = document.createElement('p');
p.appendChild(document.createTextNode(str));
logContainer.appendChild(p);
}
/**
* Scroll to bottom of page if checkbox is checked
*/
function scrollToBottom() {
if (followLogsBtn.checked) {
window.scrollTo(0, document.body.scrollHeight);
}
}
/**
* Establish websocket connection
*/
function connect() {
const loc = window.location;
let new_uri = loc.protocol === 'https:' ? 'wss:' : 'ws:';
new_uri += "//" + loc.host;
new_uri += wsPath(loc);
var ws = new WebSocket(new_uri);
ws.onmessage = function (message) {
console.log('Got message: ', message);
const buildEvent = JSON.parse(message.data);
if (buildEvent.type === 'finish') {
ws.close();
buildStatusTxt.replaceChild(document.createTextNode(buildEvent.message), buildStatusTxt.firstChild);
}
else {
appendLine(buildEvent.message, buildEvent.type === 'err');
scrollToBottom();
}
}
}
connect();
followLogsBtn.checked = false;
followLogsBtn.addEventListener('change', scrollToBottom);
});

View File

@ -19,10 +19,11 @@ class BuildController extends EventEmitter {
private db: DB;
private process: spawn.ChildProcess | null = null;
private running: boolean = false;
private interval: NodeJS.Timeout;
constructor(config = {}) {
super();
setInterval(this.triggerBuild, 60000);
// this.interval = setInterval(this.triggerBuild, 60000);
}
triggerBuild = () => {
@ -106,18 +107,18 @@ class BuildController extends EventEmitter {
});
docker.on('close', (code) => {
this.process = null;
const status = code === 0 ? 'success' : 'error';
this.emitLog({
id: build.id,
type: 'finish',
message: code
message: status
});
this.db.finishBuild(build.id, status);
if (code === 0) {
this.db.finishBuild(build.id, 'success');
resolve();
}
else {
this.db.finishBuild(build.id, 'error');
reject(code);
}
});
@ -149,6 +150,12 @@ class BuildController extends EventEmitter {
setDB = (db: DB) => {
this.db = db;
}
close = () => {
if (this.interval) {
clearInterval(this.interval);
}
}
}
export default BuildController;

View File

@ -1,10 +1,11 @@
import * as http from "http";
import crypto from 'crypto';
import type { Express } from "express";
import express, { application } from 'express';
import express from 'express';
import expressWs from "express-ws";
import bodyParser from "body-parser";
import type { DB } from "./DB.ts";
import type { BuildController } from "./BuildController.ts";
import type { BuildController, BuildEvent } from "./BuildController.ts";
interface WebConfig {
port?: number;
@ -26,6 +27,7 @@ class Web {
constructor(options: WebConfig = {}) {
const app: Express = this.app = express();
const wsApp = expressWs(app).app;
this.port = notStupidParseInt(process.env.PORT) || options['port'] as number || 8080;
app.set('trust proxy', 1);
@ -95,7 +97,8 @@ class Web {
description: `Building ${build.repo} on ${build.distro}`
},
build,
log
log,
ended: build.status !== 'queued' && build.status !== 'running'
});
});
@ -112,6 +115,22 @@ class Web {
app.get('/healthcheck', (_, res) => {
res.send('Healthy');
});
wsApp.ws('/build/:num/ws', (ws, req) => {
console.log('WS Opened');
const eventListener = (be: BuildEvent) => {
if (be.id === notStupidParseInt(req.params.num)) {
ws.send(JSON.stringify(be));
}
};
this.buildController.on('log', eventListener);
ws.on('close', () => {
console.log('WS Closed');
this.buildController.removeListener('log', eventListener);
});
});
}
close = () => {

View File

@ -24,4 +24,5 @@ buildController.setDB(db);
process.on('SIGTERM', () => {
web.close();
db.close();
buildController.close();
});

View File

@ -105,11 +105,13 @@ input[type="submit"] {
display: inline-block;
padding: .3em .6em;
color: #fff;
margin-bottom: .4em;
box-shadow: 0px .4em 0 0 var(--primary-dark);
&:active {
margin-top: .4em;
margin-bottom: 0;
box-shadow: 0px .1em 0 0 var(--primary-dark);
}
}
@ -238,3 +240,17 @@ input[type="submit"] {
}
}
}
#followCheckmarkContainer {
display: inline-block;
position: fixed;
right: 1.3em;
bottom: 1.6em;
padding: 1em;
background-color: rgba(15,15,15,.8);
cursor: pointer;
input {
cursor: inherit;
}
}

View File

@ -9,12 +9,13 @@
<%- include("navigation", locals) %>
<div class="content">
<h1>Build #<%= build.id %></h1>
<h2><%= build.status %></h2>
<h2 id="buildStatus"><%= build.status %></h2>
<div class="grid-2col">
<label>Repo</label> <span><%= build.repo %></span>
<label>Commit</label> <span><% if (build.commit) { %><%= build.commit %><% } else { %>latest<% } %></span>
<label>Patch</label> <span><% if (build.patch) { %><a href="/build/<%= build.id %>/patch">patch file</a><% } else { %>none<% } %></span>
<label>Distro</label> <span><%= build.distro %></span>
<label>Dependencies</label> <span><%= build.dependencies %></span>
<label>Start time</label> <span><%= build.startTime %></span>
</div>
<p><a href="/build/<%= build.id %>/logs">Full logs</a></p>
@ -23,7 +24,16 @@
<p><%= line %></p>
<% }) %>
</div>
<% if (!ended) { %>
<label id="followCheckmarkContainer" title="Follow logs">
<input type="checkbox" id="followCheckmark" />
</label>
<% } %>
</div>
<%- include("footer", locals) %>
<% if (!ended) { %>
<script src="/assets/js/build.js?v1" nonce="<%= cspNonce %>"></script>
<% } %>
</body>
</html>

View File

@ -9,11 +9,11 @@
</div>
<nav>
<ul class="sidebar_links">
<li><a href="/build">New Build</a></li>
<li><a href="/">All Builds</a></li>
<li><a href="/?status=error">Failed Builds</a></li>
<li><a href="/?distro=arch">Arch Builds</a></li>
<li><a href="/?distro=artix">Artix Builds</a></li>
<li><a href="/build">New Build</a></li>
</ul>
</nav>
</div>