mirror of
https://gitlab.com/wgp/dougal/software.git
synced 2025-12-06 09:07:09 +00:00
Compare commits
11 Commits
2fab06d340
...
v2025.33.4
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8e4e70cbdc | ||
|
|
4dadffbbe7 | ||
|
|
24dcebd0d9 | ||
|
|
12a762f44f | ||
|
|
ebf13abc28 | ||
|
|
b3552db02f | ||
|
|
cd882c0611 | ||
|
|
6fc9c020a4 | ||
|
|
75284322f1 | ||
|
|
e849c47f01 | ||
|
|
387d20a4f0 |
@@ -693,7 +693,7 @@ class DougalBinaryChunkSequential extends ArrayBuffer {
|
||||
getRecord (index) {
|
||||
if (index < 0 || index >= this.jCount) throw new Error(`Invalid record index: ${index}`);
|
||||
|
||||
const arr = [thid.udv, this.i, this.j0 + index * this.Δj];
|
||||
const arr = [this.udv, this.i, this.j0 + index * this.Δj];
|
||||
|
||||
for (let m = 0; m < this.ΔelemCount; m++) {
|
||||
const values = this.Δelem(m);
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
<v-dialog
|
||||
v-model="dialog"
|
||||
max-width="500"
|
||||
scrollable
|
||||
style="z-index:2020;"
|
||||
>
|
||||
<template v-slot:activator="{ on, attrs }">
|
||||
@@ -58,6 +59,9 @@
|
||||
|
||||
|
||||
</v-window-item>
|
||||
<v-window-item value="serverinfo">
|
||||
<dougal-server-status :status="serverStatus"></dougal-server-status>
|
||||
</v-window-item>
|
||||
</v-window>
|
||||
|
||||
<v-divider></v-divider>
|
||||
@@ -69,8 +73,7 @@
|
||||
text
|
||||
:href="`mailto:${email}?Subject=Question`"
|
||||
>
|
||||
<v-icon class="d-lg-none">mdi-help-circle</v-icon>
|
||||
<span class="d-none d-lg-inline">Ask a question</span>
|
||||
<v-icon title="Ask a question">mdi-help-circle</v-icon>
|
||||
</v-btn>
|
||||
|
||||
<v-btn
|
||||
@@ -78,8 +81,7 @@
|
||||
text
|
||||
href="mailto:dougal-support@aaltronav.eu?Subject=Bug report"
|
||||
>
|
||||
<v-icon class="d-lg-none">mdi-bug</v-icon>
|
||||
<span class="d-none d-lg-inline">Report a bug</span>
|
||||
<v-icon title="Report a bug">mdi-bug</v-icon>
|
||||
</v-btn>
|
||||
|
||||
<!---
|
||||
@@ -93,16 +95,36 @@
|
||||
</v-btn>
|
||||
--->
|
||||
|
||||
<v-btn
|
||||
color="info"
|
||||
text
|
||||
title="View support info"
|
||||
:input-value="page == 'support'"
|
||||
@click="page = 'support'"
|
||||
>
|
||||
<v-icon>mdi-account-question</v-icon>
|
||||
</v-btn>
|
||||
|
||||
<v-btn v-if="versionHistory"
|
||||
color="info"
|
||||
text
|
||||
:title="page == 'support' ? 'View release notes' : 'View support info'"
|
||||
title="View release notes"
|
||||
:input-value="page == 'changelog'"
|
||||
@click="page = page == 'support' ? 'changelog' : 'support'"
|
||||
@click="page = 'changelog'"
|
||||
>
|
||||
<v-icon>mdi-history</v-icon>
|
||||
</v-btn>
|
||||
|
||||
<v-btn v-if="serverStatus"
|
||||
color="info"
|
||||
text
|
||||
title="View server status"
|
||||
:input-value="page == 'serverinfo'"
|
||||
@click="page = 'serverinfo'"
|
||||
>
|
||||
<v-icon>mdi-server-network</v-icon>
|
||||
</v-btn>
|
||||
|
||||
<v-spacer></v-spacer>
|
||||
|
||||
|
||||
@@ -124,46 +146,110 @@
|
||||
|
||||
<script>
|
||||
import { mapActions, mapGetters } from 'vuex';
|
||||
import DougalServerStatus from './server-status';
|
||||
|
||||
export default {
|
||||
name: 'DougalHelpDialog',
|
||||
|
||||
components: {
|
||||
DougalServerStatus
|
||||
},
|
||||
|
||||
data () {
|
||||
return {
|
||||
dialog: false,
|
||||
email: "dougal-support@aaltronav.eu",
|
||||
feed: btoa(encodeURIComponent("https://gitlab.com/wgp/dougal/software.atom?feed_token=XSPpvsYEny8YmH75Nz5W")),
|
||||
serverStatus: null,
|
||||
clientVersion: process.env.DOUGAL_FRONTEND_VERSION ?? "(unknown)",
|
||||
serverVersion: null,
|
||||
versionHistory: null,
|
||||
releaseHistory: [],
|
||||
releaseShown: null,
|
||||
page: "support"
|
||||
page: "support",
|
||||
|
||||
lastUpdate: 0,
|
||||
updateInterval: 12000,
|
||||
refreshTimer: null
|
||||
};
|
||||
},
|
||||
|
||||
computed: {
|
||||
sinceUpdate () {
|
||||
return this.lastUpdate
|
||||
? (Date.now() - this.lastUpdate)
|
||||
: +Infinity;
|
||||
}
|
||||
},
|
||||
|
||||
watch: {
|
||||
dialog(newVal) {
|
||||
if (newVal) {
|
||||
this.startAutoRefresh();
|
||||
} else {
|
||||
this.stopAutoRefresh();
|
||||
}
|
||||
},
|
||||
page(newVal) {
|
||||
if (newVal === 'serverinfo' && this.dialog) {
|
||||
this.getServerStatus(); // Immediate update when switching to serverinfo
|
||||
this.startAutoRefresh();
|
||||
} else {
|
||||
this.stopAutoRefresh();
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
methods: {
|
||||
async getServerVersion () {
|
||||
if (!this.serverVersion) {
|
||||
const version = await this.api(['/version', {}, null, {silent:true}]);
|
||||
this.serverVersion = version?.tag ?? "(unknown)";
|
||||
if (version) this.lastUpdate = Date.now();
|
||||
}
|
||||
if (!this.versionHistory) {
|
||||
const history = await this.api(['/version/history?count=3', {}, null, {silent:true}]);
|
||||
const history = await this.api(['/version/history?count=6', {}, null, {silent:true}]);
|
||||
this.releaseHistory = history;
|
||||
this.versionHistory = history?.[this.serverVersion.replace(/-.*$/, "")] ?? null;
|
||||
}
|
||||
},
|
||||
|
||||
async getServerStatus () {
|
||||
const status = await this.api(['/diagnostics', {}, null, {silent: true}]);
|
||||
if (status) {
|
||||
this.serverStatus = status;
|
||||
this.lastUpdate = Date.now();
|
||||
}
|
||||
},
|
||||
|
||||
startAutoRefresh() {
|
||||
if (this.refreshTimer) return; // Prevent multiple timers
|
||||
this.refreshTimer = setInterval(() => {
|
||||
if (this.dialog && this.page === 'serverinfo') {
|
||||
this.getServerStatus();
|
||||
// Optionally refresh server version if needed
|
||||
// this.getServerVersion();
|
||||
}
|
||||
}, this.updateInterval);
|
||||
},
|
||||
|
||||
stopAutoRefresh() {
|
||||
if (this.refreshTimer) {
|
||||
clearInterval(this.refreshTimer);
|
||||
this.refreshTimer = null;
|
||||
}
|
||||
},
|
||||
|
||||
...mapActions(["api"])
|
||||
},
|
||||
|
||||
async mounted () {
|
||||
this.getServerVersion();
|
||||
this.getServerStatus();
|
||||
},
|
||||
|
||||
async beforeUpdate () {
|
||||
this.getServerVersion();
|
||||
beforeDestroy() {
|
||||
this.stopAutoRefresh(); // Clean up timer on component destruction
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
213
lib/www/client/source/src/components/server-status.vue
Normal file
213
lib/www/client/source/src/components/server-status.vue
Normal file
@@ -0,0 +1,213 @@
|
||||
<template>
|
||||
<v-card max-width="800" max-height="600" class="mx-auto" style="overflow-y: auto;">
|
||||
<v-card-title class="headline">
|
||||
Server status – {{ status.hostname }}
|
||||
</v-card-title>
|
||||
<v-card-text>
|
||||
<v-expansion-panels accordion>
|
||||
<!-- System Info -->
|
||||
<v-expansion-panel>
|
||||
<v-expansion-panel-header>System Info</v-expansion-panel-header>
|
||||
<v-expansion-panel-content>
|
||||
<v-row>
|
||||
<v-col cols="6">
|
||||
<strong>Uptime:</strong> {{ formatUptime(status.uptime) }}
|
||||
</v-col>
|
||||
<v-col cols="6">
|
||||
<strong>Load:</strong> {{ status.loadavg[0].toFixed(2) }} / {{ status.loadavg[1].toFixed(2) }} / {{ status.loadavg[2].toFixed(2) }}
|
||||
<v-progress-linear
|
||||
:value="loadAvgPercent"
|
||||
:color="getLoadAvgColor(status.loadavg[0])"
|
||||
height="6"
|
||||
rounded
|
||||
></v-progress-linear>
|
||||
<div class="text-caption">
|
||||
1-min Load: {{ status.loadavg[0].toFixed(2) }} ({{ loadAvgPercent.toFixed(1) }}% of max)
|
||||
</div>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</v-expansion-panel-content>
|
||||
</v-expansion-panel>
|
||||
|
||||
<!-- Memory -->
|
||||
<v-expansion-panel>
|
||||
<v-expansion-panel-header>Memory</v-expansion-panel-header>
|
||||
<v-expansion-panel-content>
|
||||
<v-progress-linear
|
||||
:value="memoryUsedPercent"
|
||||
:color="getProgressColor(memoryUsedPercent)"
|
||||
height="10"
|
||||
rounded
|
||||
></v-progress-linear>
|
||||
<div class="text-caption mt-2">
|
||||
Used: {{ formatBytes(status.memory.total - status.memory.free) }} / Total: {{ formatBytes(status.memory.total) }} ({{ memoryUsedPercent.toFixed(1) }}%)
|
||||
</div>
|
||||
</v-expansion-panel-content>
|
||||
</v-expansion-panel>
|
||||
|
||||
<!-- CPUs -->
|
||||
<v-expansion-panel>
|
||||
<v-expansion-panel-header>CPUs ({{ status.cpus.length }} cores)</v-expansion-panel-header>
|
||||
<v-expansion-panel-content>
|
||||
<v-row dense>
|
||||
<v-col v-for="(cpu, index) in status.cpus" :key="index" cols="12" sm="6">
|
||||
<v-card outlined class="pa-2">
|
||||
<div class="text-caption">Core {{ index + 1 }}: {{ cpu.model }} @ {{ cpu.speed }} MHz</div>
|
||||
<v-progress-linear
|
||||
:value="cpuUsagePercent(cpu)"
|
||||
:color="getProgressColor(cpuUsagePercent(cpu))"
|
||||
height="8"
|
||||
rounded
|
||||
></v-progress-linear>
|
||||
<div class="text-caption">
|
||||
Usage: {{ cpuUsagePercent(cpu).toFixed(1) }}% (Idle: {{ cpuIdlePercent(cpu).toFixed(1) }}%)
|
||||
</div>
|
||||
</v-card>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</v-expansion-panel-content>
|
||||
</v-expansion-panel>
|
||||
|
||||
<!-- Network Interfaces -->
|
||||
<v-expansion-panel>
|
||||
<v-expansion-panel-header>Network Interfaces</v-expansion-panel-header>
|
||||
<v-expansion-panel-content>
|
||||
<v-list dense>
|
||||
<v-list-item v-for="(iface, name) in status.networkInterfaces" :key="name">
|
||||
<v-list-item-content>
|
||||
<v-list-item-title>{{ name }}</v-list-item-title>
|
||||
<v-list-item-subtitle v-for="(addr, idx) in iface" :key="idx">
|
||||
{{ addr.family }}: {{ addr.address }} (Netmask: {{ addr.netmask }})
|
||||
</v-list-item-subtitle>
|
||||
</v-list-item-content>
|
||||
</v-list-item>
|
||||
</v-list>
|
||||
</v-expansion-panel-content>
|
||||
</v-expansion-panel>
|
||||
|
||||
<!-- Storage -->
|
||||
<v-expansion-panel>
|
||||
<v-expansion-panel-header>Storage</v-expansion-panel-header>
|
||||
<v-expansion-panel-content>
|
||||
<!-- Root -->
|
||||
<div class="mb-4">
|
||||
<strong>Root (/):</strong>
|
||||
<v-progress-linear
|
||||
:value="status.storage.root.usedPercent"
|
||||
:color="getProgressColor(status.storage.root.usedPercent)"
|
||||
height="10"
|
||||
rounded
|
||||
></v-progress-linear>
|
||||
<div class="text-caption">
|
||||
Used: {{ formatBytes(status.storage.root.used) }} / Total: {{ formatBytes(status.storage.root.total) }} ({{ status.storage.root.usedPercent.toFixed(1) }}%)
|
||||
</div>
|
||||
</div>
|
||||
<!-- Data subfolders -->
|
||||
<div>
|
||||
<strong>Data:</strong>
|
||||
<v-expansion-panels flat>
|
||||
<v-expansion-panel v-for="(folder, name) in status.storage.data" :key="name">
|
||||
<v-expansion-panel-header disable-icon-rotate>{{ name }}</v-expansion-panel-header>
|
||||
<v-expansion-panel-content>
|
||||
<v-progress-linear
|
||||
:value="folder.usedPercent"
|
||||
:color="getProgressColor(folder.usedPercent)"
|
||||
height="10"
|
||||
rounded
|
||||
></v-progress-linear>
|
||||
<div class="text-caption">
|
||||
Used: {{ formatBytes(folder.used) }} / Total: {{ formatBytes(folder.total) }} ({{ folder.usedPercent.toFixed(1) }}%)
|
||||
</div>
|
||||
</v-expansion-panel-content>
|
||||
</v-expansion-panel>
|
||||
</v-expansion-panels>
|
||||
</div>
|
||||
</v-expansion-panel-content>
|
||||
</v-expansion-panel>
|
||||
|
||||
<!-- Database -->
|
||||
<v-expansion-panel>
|
||||
<v-expansion-panel-header>Database</v-expansion-panel-header>
|
||||
<v-expansion-panel-content>
|
||||
<div class="mb-2">
|
||||
<strong>Total Size:</strong> {{ formatBytes(status.database.size) }}
|
||||
</div>
|
||||
<v-list dense>
|
||||
<v-list-item v-for="(project, name) in status.database.projects" :key="name">
|
||||
<v-list-item-content>
|
||||
<v-list-item-title>{{ name }}</v-list-item-title>
|
||||
<v-progress-linear
|
||||
:value="project.percent"
|
||||
:color="getProgressColor(project.percent)"
|
||||
height="8"
|
||||
rounded
|
||||
></v-progress-linear>
|
||||
<v-list-item-subtitle>
|
||||
Size: {{ formatBytes(project.size) }} ({{ project.percent.toFixed(2) }}%)
|
||||
</v-list-item-subtitle>
|
||||
</v-list-item-content>
|
||||
</v-list-item>
|
||||
</v-list>
|
||||
</v-expansion-panel-content>
|
||||
</v-expansion-panel>
|
||||
</v-expansion-panels>
|
||||
</v-card-text>
|
||||
</v-card>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: "DougalServerStatus",
|
||||
props: {
|
||||
status: {
|
||||
type: Object,
|
||||
required: true
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
memoryUsedPercent() {
|
||||
return ((this.status.memory.total - this.status.memory.free) / this.status.memory.total) * 100;
|
||||
},
|
||||
loadAvgPercent() {
|
||||
const maxLoad = this.status.cpus.length * 4; // Assume 4x cores as max for scaling
|
||||
return Math.min((this.status.loadavg[0] / maxLoad) * 100, 100); // Cap at 100%
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
getProgressColor(value) {
|
||||
if (value >= 80) return 'error'; // Red for 80–100%
|
||||
if (value >= 60) return 'warning'; // Yellow for 60–80%
|
||||
return 'success'; // Green for 0–60%
|
||||
},
|
||||
getLoadAvgColor(load) {
|
||||
const coreCount = this.status.cpus.length;
|
||||
if (load >= coreCount * 2) return 'error'; // Red for load ≥ 2x cores
|
||||
if (load >= coreCount) return 'warning'; // Yellow for load ≥ 1x cores but < 2x
|
||||
return 'success'; // Green for load < 1x cores
|
||||
},
|
||||
formatBytes(bytes) {
|
||||
if (bytes === 0) return '0 Bytes';
|
||||
const k = 1024;
|
||||
const sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB', 'PB'];
|
||||
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
||||
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
|
||||
},
|
||||
formatUptime(seconds) {
|
||||
const days = Math.floor(seconds / 86400);
|
||||
seconds %= 86400;
|
||||
const hours = Math.floor(seconds / 3600);
|
||||
seconds %= 3600;
|
||||
const minutes = Math.floor(seconds / 60);
|
||||
return `${days}d ${hours}h ${minutes}m`;
|
||||
},
|
||||
cpuUsagePercent(cpu) {
|
||||
const total = cpu.times.user + cpu.times.nice + cpu.times.sys + cpu.times.idle + cpu.times.irq;
|
||||
return ((total - cpu.times.idle) / total) * 100;
|
||||
},
|
||||
cpuIdlePercent(cpu) {
|
||||
const total = cpu.times.user + cpu.times.nice + cpu.times.sys + cpu.times.idle + cpu.times.irq;
|
||||
return (cpu.times.idle / total) * 100;
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
@@ -66,8 +66,18 @@ const rels = [
|
||||
|
||||
function invalidateCache (data, cache) {
|
||||
return new Promise((resolve, reject) => {
|
||||
if (!data) {
|
||||
ERROR("invalidateCache called with no data");
|
||||
return;
|
||||
}
|
||||
|
||||
if (!data.payload) {
|
||||
ERROR("invalidateCache called without a payload; channel = %s", data.channel);
|
||||
return;
|
||||
}
|
||||
|
||||
const channel = data.channel;
|
||||
const project = data.payload.pid ?? data.payload?.new?.pid ?? data.payload?.old?.pid;
|
||||
const project = data.payload?.pid ?? data.payload?.new?.pid ?? data.payload?.old?.pid;
|
||||
const operation = data.payload.operation;
|
||||
const table = data.payload.table;
|
||||
const fields = { channel, project, operation, table };
|
||||
|
||||
@@ -16,7 +16,6 @@ module.exports = async function (req, res, next) {
|
||||
|
||||
if (json.length) {
|
||||
const data = bundle(json, {type});
|
||||
console.log("bundle", data);
|
||||
res.status(200).send(Buffer.from(data));
|
||||
} else {
|
||||
res.status(404).send();
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
const project = require('../../lib/db/project');
|
||||
const { ALERT, ERROR, WARNING, NOTICE, INFO, DEBUG } = require('DOUGAL_ROOT/debug')(__filename);
|
||||
|
||||
class DetectProjectConfigurationChange {
|
||||
@@ -10,7 +9,7 @@ class DetectProjectConfigurationChange {
|
||||
|
||||
// Grab project configurations.
|
||||
// NOTE that this will run asynchronously
|
||||
this.run({channel: "project"}, ctx);
|
||||
//this.run({channel: "project"}, ctx);
|
||||
}
|
||||
|
||||
async run (data, ctx) {
|
||||
@@ -28,13 +27,13 @@ class DetectProjectConfigurationChange {
|
||||
try {
|
||||
DEBUG("Project configuration change detected")
|
||||
|
||||
const projects = await project.get();
|
||||
project.organisations.setCache(projects);
|
||||
const projects = await ctx.db.project.get();
|
||||
ctx.db.project.organisations.setCache(projects);
|
||||
|
||||
const _ctx_data = {};
|
||||
for (let pid of projects.map(i => i.pid)) {
|
||||
DEBUG("Retrieving configuration for", pid);
|
||||
const cfg = await project.configuration.get(pid);
|
||||
const cfg = await ctx.db.project.configuration.get(pid);
|
||||
if (cfg?.archived === true) {
|
||||
DEBUG(pid, "is archived. Ignoring");
|
||||
continue;
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
const { schema2pid } = require('../../lib/db/connection');
|
||||
const { event } = require('../../lib/db');
|
||||
const { ALERT, ERROR, WARNING, NOTICE, INFO, DEBUG } = require('DOUGAL_ROOT/debug')(__filename);
|
||||
|
||||
class DetectSoftStart {
|
||||
@@ -33,14 +31,19 @@ class DetectSoftStart {
|
||||
const prev = this.prev?.payload?.new?.meta;
|
||||
// DEBUG("%j", prev);
|
||||
// DEBUG("%j", cur);
|
||||
DEBUG("cur.num_guns: %d\ncur.num_active: %d\nprv.num_active: %d\ntest passed: %j", cur.num_guns, cur.num_active, prev.num_active, cur.num_active >= 1 && !prev.num_active && cur.num_active < cur.num_guns);
|
||||
if (cur.lineStatus == "online" || prev.lineStatus == "online") {
|
||||
DEBUG("lineStatus is online, assuming not in a soft start situation");
|
||||
return;
|
||||
}
|
||||
|
||||
DEBUG("cur.num_guns: %d\ncur.num_active: %d\nprv.num_active: %d\ncur.num_nofire: %d\nprev.num_nofire: %d", cur.num_guns, cur.num_active, prev.num_active, cur.num_nofire, prev.num_nofire);
|
||||
|
||||
|
||||
if (cur.num_active >= 1 && !prev.num_active && cur.num_active < cur.num_guns) {
|
||||
INFO("Soft start detected @", cur.tstamp);
|
||||
|
||||
// FIXME Shouldn't need to use schema2pid as pid already present in payload.
|
||||
const projectId = await schema2pid(cur._schema ?? prev._schema);
|
||||
const projectId = await ctx.schema2pid(cur._schema ?? prev._schema);
|
||||
|
||||
// TODO: Try and grab the corresponding comment from the configuration?
|
||||
const payload = {
|
||||
@@ -50,12 +53,16 @@ class DetectSoftStart {
|
||||
meta: {auto: true, author: `*${this.constructor.name}*`}
|
||||
};
|
||||
DEBUG("Posting event", projectId, payload);
|
||||
await event.post(projectId, payload);
|
||||
if (ctx.dryRun) {
|
||||
DEBUG(`DRY RUN: await ctx.db.event.post(${projectId}, ${payload});`);
|
||||
} else {
|
||||
await ctx.db.event.post(projectId, payload);
|
||||
}
|
||||
|
||||
} else if (cur.num_active == cur.num_guns && prev.num_active < cur.num_active) {
|
||||
} else if ((cur.num_active == cur.num_guns || (prev.num_nofire > 0 && cur.num_nofire == 0)) && prev.num_active < cur.num_active) {
|
||||
INFO("Full volume detected @", cur.tstamp);
|
||||
|
||||
const projectId = await schema2pid(cur._schema ?? prev._schema);
|
||||
const projectId = await ctx.schema2pid(cur._schema ?? prev._schema);
|
||||
|
||||
// TODO: Try and grab the corresponding comment from the configuration?
|
||||
const payload = {
|
||||
@@ -65,7 +72,11 @@ class DetectSoftStart {
|
||||
meta: {auto: true, author: `*${this.constructor.name}*`}
|
||||
};
|
||||
DEBUG("Posting event", projectId, payload);
|
||||
await event.post(projectId, payload);
|
||||
if (ctx.dryRun) {
|
||||
DEBUG(`DRY RUN: await ctx.db.event.post(${projectId}, ${payload});`);
|
||||
} else {
|
||||
await ctx.db.event.post(projectId, payload);
|
||||
}
|
||||
}
|
||||
|
||||
} catch (err) {
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
const { schema2pid } = require('../../lib/db/connection');
|
||||
const { event } = require('../../lib/db');
|
||||
const { ALERT, ERROR, WARNING, NOTICE, INFO, DEBUG } = require('DOUGAL_ROOT/debug')(__filename);
|
||||
|
||||
class DetectSOLEOL {
|
||||
@@ -43,7 +41,7 @@ class DetectSOLEOL {
|
||||
|
||||
// We must use schema2pid because the pid may not have been
|
||||
// populated for this event.
|
||||
const projectId = await schema2pid(cur._schema ?? prev._schema);
|
||||
const projectId = await ctx.schema2pid(cur._schema ?? prev._schema);
|
||||
const labels = ["FSP", "FGSP"];
|
||||
const remarks = `SEQ ${cur._sequence}, SOL ${cur.lineName}, BSP: ${(cur.speed*3.6/1.852).toFixed(1)} kt, Water depth: ${Number(cur.waterDepth).toFixed(0)} m.`;
|
||||
const payload = {
|
||||
@@ -55,24 +53,32 @@ class DetectSOLEOL {
|
||||
meta: {auto: true, author: `*${this.constructor.name}*`}
|
||||
}
|
||||
INFO("Posting event", projectId, payload);
|
||||
await event.post(projectId, payload);
|
||||
if (ctx.dryRun) {
|
||||
DEBUG(`DRY RUN: await ctx.db.event.post(${projectId}, ${payload});`);
|
||||
} else {
|
||||
await ctx.db.event.post(projectId, payload);
|
||||
}
|
||||
} else if (prev.lineName == cur.lineName && prev._sequence == cur._sequence &&
|
||||
prev.lineStatus == "online" && cur.lineStatus != "online" && sequence) {
|
||||
INFO("Transition to OFFLINE detected");
|
||||
|
||||
const projectId = await schema2pid(prev._schema ?? cur._schema);
|
||||
const projectId = await ctx.schema2pid(prev._schema ?? cur._schema);
|
||||
const labels = ["LSP", "LGSP"];
|
||||
const remarks = `SEQ ${cur._sequence}, EOL ${cur.lineName}, BSP: ${(cur.speed*3.6/1.852).toFixed(1)} kt, Water depth: ${Number(cur.waterDepth).toFixed(0)} m.`;
|
||||
const remarks = `SEQ ${prev._sequence}, EOL ${prev.lineName}, BSP: ${(prev.speed*3.6/1.852).toFixed(1)} kt, Water depth: ${Number(prev.waterDepth).toFixed(0)} m.`;
|
||||
const payload = {
|
||||
type: "sequence",
|
||||
sequence,
|
||||
point: cur._point,
|
||||
point: prev._point,
|
||||
remarks,
|
||||
labels,
|
||||
meta: {auto: true, author: `*${this.constructor.name}*`}
|
||||
}
|
||||
INFO("Posting event", projectId, payload);
|
||||
await event.post(projectId, payload);
|
||||
if (ctx.dryRun) {
|
||||
DEBUG(`DRY RUN: await ctx.db.event.post(${projectId}, ${payload});`);
|
||||
} else {
|
||||
await ctx.db.event.post(projectId, payload);
|
||||
}
|
||||
}
|
||||
|
||||
} catch (err) {
|
||||
|
||||
@@ -8,37 +8,6 @@ const Handlers = [
|
||||
require('./detect-fdsp')
|
||||
];
|
||||
|
||||
function init (ctx) {
|
||||
|
||||
const instances = Handlers.map(Handler => new Handler(ctx));
|
||||
|
||||
function prepare (data, ctx) {
|
||||
const promises = [];
|
||||
for (let instance of instances) {
|
||||
const promise = new Promise(async (resolve, reject) => {
|
||||
try {
|
||||
DEBUG("Run", instance.author);
|
||||
const result = await instance.run(data, ctx);
|
||||
DEBUG("%s result: %O", instance.author, result);
|
||||
resolve(result);
|
||||
} catch (err) {
|
||||
ERROR("%s error:\n%O", instance.author, err);
|
||||
reject(err);
|
||||
}
|
||||
});
|
||||
promises.push(promise);
|
||||
}
|
||||
return promises;
|
||||
}
|
||||
|
||||
function despatch (data, ctx) {
|
||||
return Promise.allSettled(prepare(data, ctx));
|
||||
}
|
||||
|
||||
return { instances, prepare, despatch };
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
Handlers,
|
||||
init
|
||||
};
|
||||
|
||||
@@ -1,6 +1,3 @@
|
||||
const { event, project } = require('../../lib/db');
|
||||
const { withinValidity } = require('../../lib/utils/ranges');
|
||||
const unique = require('../../lib/utils/unique');
|
||||
const { ALERT, ERROR, WARNING, NOTICE, INFO, DEBUG } = require('DOUGAL_ROOT/debug')(__filename);
|
||||
|
||||
class ReportLineChangeTime {
|
||||
@@ -44,7 +41,7 @@ class ReportLineChangeTime {
|
||||
|
||||
async function getLineChangeTime (data, forward = false) {
|
||||
if (forward) {
|
||||
const ospEvents = await event.list(projectId, {label: "FGSP"});
|
||||
const ospEvents = await ctx.db.event.list(projectId, {label: "FGSP"});
|
||||
// DEBUG("ospEvents", ospEvents);
|
||||
const osp = ospEvents.filter(i => i.tstamp > data.tstamp).pop();
|
||||
DEBUG("fsp", osp);
|
||||
@@ -55,7 +52,7 @@ class ReportLineChangeTime {
|
||||
return { lineChangeTime: osp.tstamp - data.tstamp, osp };
|
||||
}
|
||||
} else {
|
||||
const ospEvents = await event.list(projectId, {label: "LGSP"});
|
||||
const ospEvents = await ctx.db.event.list(projectId, {label: "LGSP"});
|
||||
// DEBUG("ospEvents", ospEvents);
|
||||
const osp = ospEvents.filter(i => i.tstamp < data.tstamp).shift();
|
||||
DEBUG("lsp", osp);
|
||||
@@ -96,16 +93,20 @@ class ReportLineChangeTime {
|
||||
const opts = {jpq};
|
||||
|
||||
if (Array.isArray(seq)) {
|
||||
opts.sequences = unique(seq).filter(i => !!i);
|
||||
opts.sequences = ctx.unique(seq).filter(i => !!i);
|
||||
} else {
|
||||
opts.sequence = seq;
|
||||
}
|
||||
|
||||
const staleEvents = await event.list(projectId, opts);
|
||||
const staleEvents = await ctx.db.event.list(projectId, opts);
|
||||
DEBUG(staleEvents.length ?? 0, "events to delete");
|
||||
for (let staleEvent of staleEvents) {
|
||||
DEBUG(`Deleting event id ${staleEvent.id} (seq = ${staleEvent.sequence}, point = ${staleEvent.point})`);
|
||||
await event.del(projectId, staleEvent.id);
|
||||
if (ctx.dryRun) {
|
||||
DEBUG(`await ctx.db.event.del(${projectId}, ${staleEvent.id});`);
|
||||
} else {
|
||||
await ctx.db.event.del(projectId, staleEvent.id);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -180,7 +181,11 @@ class ReportLineChangeTime {
|
||||
|
||||
const maybePostEvent = async (projectId, payload) => {
|
||||
DEBUG("Posting event", projectId, payload);
|
||||
await event.post(projectId, payload);
|
||||
if (ctx.dryRun) {
|
||||
DEBUG(`await ctx.db.event.post(${projectId}, ${payload});`);
|
||||
} else {
|
||||
await ctx.db.event.post(projectId, payload);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -192,7 +197,7 @@ class ReportLineChangeTime {
|
||||
const data = n;
|
||||
DEBUG("INSERT seen: will add lct events related to ", data.id);
|
||||
|
||||
if (withinValidity(data.validity)) {
|
||||
if (ctx.withinValidity(data.validity)) {
|
||||
DEBUG("Event within validity period", data.validity, new Date());
|
||||
|
||||
data.tstamp = new Date(data.tstamp);
|
||||
|
||||
@@ -1,29 +1,101 @@
|
||||
const nodeAsync = require('async'); // npm install async
|
||||
const { listen } = require('../lib/db/notify');
|
||||
const db = require('../lib/db'); // Adjust paths; include all needed DB utils
|
||||
const { schema2pid } = require('../lib/db/connection');
|
||||
const unique = require('../lib/utils/unique'); // If needed by handlers
|
||||
const withinValidity = require('../lib/utils/ranges').withinValidity; // If needed
|
||||
const { ALERT, ERROR, DEBUG } = require('DOUGAL_ROOT/debug')(__filename);
|
||||
|
||||
// List of handler classes (add more as needed)
|
||||
const handlerClasses = require('./handlers').Handlers;
|
||||
|
||||
// Channels to listen to (hardcoded for simplicity; could scan handlers for mentions)
|
||||
const channels = require('../lib/db/channels');
|
||||
const handlers = require('./handlers');
|
||||
const { ActionsQueue } = require('../lib/queue');
|
||||
const { ERROR, INFO, DEBUG } = require('DOUGAL_ROOT/debug')(__filename);
|
||||
|
||||
function start () {
|
||||
// Queue config: Process one at a time for order; max retries=3
|
||||
const eventQueue = nodeAsync.queue(async (task, callback) => {
|
||||
const { data, ctx } = task;
|
||||
DEBUG(`Processing event on channel ${data.channel} with timestamp ${data._received ?? 'unknown'}`);
|
||||
|
||||
const queue = new ActionsQueue();
|
||||
const ctx = {}; // Context object
|
||||
for (const handler of ctx.handlers) {
|
||||
try {
|
||||
await handler.run(data, ctx);
|
||||
} catch (err) {
|
||||
ERROR(`Error in handler ${handler.constructor.name}:`, err);
|
||||
// Retry logic: Could add task.retries++, re-enqueue if < max
|
||||
}
|
||||
}
|
||||
|
||||
const { prepare, despatch } = handlers.init(ctx);
|
||||
if (typeof callback === 'function') {
|
||||
// async v3.2.6+ does not use callsbacks with AsyncFunctions, but anyway
|
||||
callback();
|
||||
}
|
||||
}, 1); // Concurrency=1 for strict order
|
||||
|
||||
listen(channels, function (data) {
|
||||
DEBUG("Incoming data", data);
|
||||
eventQueue.error((err, task) => {
|
||||
ALERT(`Queue error processing task:`, err, task);
|
||||
});
|
||||
|
||||
// We don't bother awaiting
|
||||
queue.enqueue(() => despatch(data, ctx));
|
||||
DEBUG("Queue size", queue.length());
|
||||
// Main setup function (call from server init)
|
||||
async function setupEventHandlers(projectsConfig) {
|
||||
// Shared context
|
||||
const ctx = {
|
||||
dryRun: Boolean(process.env.DOUGAL_HANDLERS_DRY_RUN) ?? false, // If true, don't commit changes
|
||||
projects: { configuration: projectsConfig }, // From user config
|
||||
handlers: handlerClasses.map(Cls => new Cls()), // Instances
|
||||
// DB utils (add more as needed)
|
||||
db,
|
||||
schema2pid,
|
||||
unique,
|
||||
withinValidity
|
||||
// Add other utils, e.g., ctx.logger = DEBUG;
|
||||
};
|
||||
|
||||
// Optional: Replay recent events on startup to rebuild state
|
||||
// await replayRecentEvents(ctx);
|
||||
|
||||
// Setup listener
|
||||
const subscriber = await listen(channels, (rawData) => {
|
||||
const data = {
|
||||
...rawData,
|
||||
enqueuedAt: new Date() // For monitoring
|
||||
};
|
||||
eventQueue.push({ data, ctx });
|
||||
});
|
||||
|
||||
INFO("Events manager started");
|
||||
DEBUG('Event handler system initialized with channels:', channels);
|
||||
if (ctx.dryRun) {
|
||||
DEBUG('DRY RUNNING');
|
||||
}
|
||||
|
||||
// Return for cleanup if needed
|
||||
return {
|
||||
close: () => {
|
||||
subscriber.events.removeAllListeners();
|
||||
subscriber.close();
|
||||
eventQueue.kill();
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
module.exports = { start }
|
||||
// Optional: Replay last N events to rebuild handler state (e.g., this.prev)
|
||||
// async function replayRecentEvents(ctx) {
|
||||
// try {
|
||||
// // Example: Fetch last 10 realtime events, sorted by tstamp
|
||||
// const recentRealtime = await event.listAllProjects({ channel: 'realtime', limit: 10, sort: 'tstamp DESC' });
|
||||
// // Assume event.listAllProjects is a custom DB method; implement if needed
|
||||
//
|
||||
// // Enqueue in original order (reverse sort)
|
||||
// recentRealtime.reverse().forEach((evt) => {
|
||||
// const data = { channel: 'realtime', payload: { new: evt } };
|
||||
// eventQueue.push({ data, ctx });
|
||||
// });
|
||||
//
|
||||
// // Similarly for 'event' channel if needed
|
||||
// DEBUG('Replayed recent events for state rebuild');
|
||||
// } catch (err) {
|
||||
// ERROR('Error replaying events:', err);
|
||||
// }
|
||||
// }
|
||||
|
||||
if (require.main === module) {
|
||||
start();
|
||||
}
|
||||
module.exports = { setupEventHandlers };
|
||||
|
||||
@@ -2,18 +2,37 @@
|
||||
|
||||
const { ERROR, INFO, DEBUG } = require('DOUGAL_ROOT/debug')(__filename);
|
||||
|
||||
async function getProjectConfigurations (opts = {}) {
|
||||
const includeArchived = {includeArchived: false, ...opts};
|
||||
let projectConfigurations = {};
|
||||
try {
|
||||
const db = require('./lib/db');
|
||||
const pids = (await db.project.get())
|
||||
.filter(i => includeArchived || !i.archived)
|
||||
.map(i => i.pid);
|
||||
for (const pid of pids) {
|
||||
DEBUG(`Reading project configuration for ${pid}`);
|
||||
const cfg = await db.project.configuration.get(pid);
|
||||
projectConfigurations[pid] = cfg;
|
||||
}
|
||||
} catch (err) {
|
||||
ERROR("Failed to get project configurations");
|
||||
ERROR(err);
|
||||
}
|
||||
return projectConfigurations;
|
||||
}
|
||||
|
||||
async function main () {
|
||||
// Check that we're running against the correct database version
|
||||
const version = require('./lib/version');
|
||||
INFO("Running version", await version.describe());
|
||||
version.compatible()
|
||||
.then( (versions) => {
|
||||
.then( async (versions) => {
|
||||
try {
|
||||
const api = require('./api');
|
||||
const ws = require('./ws');
|
||||
const periodicTasks = require('./periodic-tasks').init();
|
||||
|
||||
const { fork } = require('child_process');
|
||||
const { setupEventHandlers } = require('./events');
|
||||
|
||||
const port = process.env.HTTP_PORT || 3000;
|
||||
const host = process.env.HTTP_HOST || "127.0.0.1";
|
||||
@@ -25,33 +44,31 @@ async function main () {
|
||||
|
||||
periodicTasks.start();
|
||||
|
||||
const eventManagerPath = [__dirname, "events"].join("/");
|
||||
const eventManager = fork(eventManagerPath, /*{ stdio: 'ignore' }*/);
|
||||
const projectConfigurations = await getProjectConfigurations();
|
||||
const handlerSystem = await setupEventHandlers(projectConfigurations);
|
||||
|
||||
process.on("SIGINT", async () => {
|
||||
DEBUG("Interrupted (SIGINT)");
|
||||
eventManager.kill()
|
||||
handlerSystem.close();
|
||||
await periodicTasks.cleanup();
|
||||
process.exit(0);
|
||||
})
|
||||
|
||||
process.on("SIGHUP", async () => {
|
||||
DEBUG("Stopping (SIGHUP)");
|
||||
eventManager.kill()
|
||||
handlerSystem.close();
|
||||
await periodicTasks.cleanup();
|
||||
process.exit(0);
|
||||
})
|
||||
|
||||
process.on('beforeExit', async () => {
|
||||
DEBUG("Preparing to exit");
|
||||
eventManager.kill()
|
||||
handlerSystem.close();
|
||||
await periodicTasks.cleanup();
|
||||
});
|
||||
|
||||
process.on('exit', async () => {
|
||||
DEBUG("Exiting");
|
||||
// eventManager.kill()
|
||||
// periodicTasks.cleanup();
|
||||
});
|
||||
} catch (err) {
|
||||
ERROR(err);
|
||||
|
||||
@@ -8,8 +8,6 @@ function bundle (json, opts = {}) {
|
||||
const deltas = [];
|
||||
const values = [];
|
||||
|
||||
// console.log("JSON LENGTH", json.length);
|
||||
// console.log("OPTS", geometries, payload);
|
||||
|
||||
if (type == 0) {
|
||||
/* Preplot information – sail line points
|
||||
@@ -74,7 +72,6 @@ function bundle (json, opts = {}) {
|
||||
type: Uint16Array
|
||||
});
|
||||
|
||||
console.log("JSON", json[0]);
|
||||
return encode.sequential(json, el => el.line, el => el.point, deltas, values, type)
|
||||
|
||||
} else if (type == 2) {
|
||||
@@ -222,9 +219,6 @@ function bundle (json, opts = {}) {
|
||||
type: Uint8Array
|
||||
});
|
||||
|
||||
console.log("DELTAS", deltas);
|
||||
console.log("VALUES", values);
|
||||
|
||||
return encode.sequential(json, el => el.sequence, el => el.point, deltas, values, type)
|
||||
} else if (type == 3) {
|
||||
/* Final positions and raw vs final errors:
|
||||
|
||||
@@ -1,52 +0,0 @@
|
||||
const Queue = require('./queue');
|
||||
|
||||
// Inspired by:
|
||||
// https://stackoverflow.com/questions/53540348/js-async-await-tasks-queue#53540586
|
||||
|
||||
class ActionsQueue extends Queue {
|
||||
|
||||
constructor (items = []) {
|
||||
super(items);
|
||||
|
||||
this.pending = false;
|
||||
}
|
||||
|
||||
enqueue (action) {
|
||||
return new Promise ((resolve, reject) => {
|
||||
super.enqueue({ action, resolve, reject });
|
||||
this.dequeue();
|
||||
});
|
||||
}
|
||||
|
||||
async dequeue () {
|
||||
|
||||
if (this.pending) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const item = super.dequeue();
|
||||
|
||||
if (!item) {
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
|
||||
this.pending = true;
|
||||
|
||||
const result = await item.action(this);
|
||||
|
||||
this.pending = false;
|
||||
item.resolve(result);
|
||||
} catch (err) {
|
||||
this.pending = false;
|
||||
item.reject(err);
|
||||
} finally {
|
||||
this.dequeue();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
module.exports = ActionsQueue;
|
||||
@@ -1,6 +0,0 @@
|
||||
|
||||
module.exports = {
|
||||
Queue: require('./queue'),
|
||||
ActionsQueue: require('./actions-queue')
|
||||
};
|
||||
|
||||
@@ -1,22 +0,0 @@
|
||||
|
||||
class Queue {
|
||||
|
||||
constructor (items = []) {
|
||||
this.items = items;
|
||||
}
|
||||
|
||||
enqueue (item) {
|
||||
this.items.push(item);
|
||||
}
|
||||
|
||||
dequeue () {
|
||||
return this.items.shift();
|
||||
}
|
||||
|
||||
length () {
|
||||
return this.items.length;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
module.exports = Queue;
|
||||
@@ -1,52 +1,110 @@
|
||||
// TODO Append location to PATH
|
||||
|
||||
const path = require('path');
|
||||
const fs = require('fs');
|
||||
const {Builder, By, Key, until} = require('selenium-webdriver');
|
||||
const firefox = require('selenium-webdriver/firefox');
|
||||
const { Builder, By, Key, until } = require('selenium-webdriver');
|
||||
const firefox = require('selenium-webdriver/firefox');
|
||||
const { execSync } = require('child_process');
|
||||
|
||||
const geckodriverPath = path.resolve(__dirname, "geckodriver");
|
||||
|
||||
// We launch a browser instance and then start an activity timer.
|
||||
// We shut down the browser after a period of inactivity, to
|
||||
// save memory.
|
||||
// State to prevent race conditions
|
||||
let driver = null;
|
||||
let timer = null;
|
||||
let isShuttingDown = false;
|
||||
|
||||
function resetTimer () {
|
||||
clearTimeout(timer);
|
||||
timer = setTimeout(shutdown, 120000); // Yup, hardcoded to two minutes. For now anyway
|
||||
// Verify GeckoDriver exists
|
||||
if (!fs.existsSync(geckodriverPath)) {
|
||||
throw new Error(`GeckoDriver not found at ${geckodriverPath}`);
|
||||
}
|
||||
|
||||
async function launch () {
|
||||
function resetTimer() {
|
||||
clearTimeout(timer);
|
||||
timer = setTimeout(shutdown, 120000); // 2 minutes inactivity timeout
|
||||
}
|
||||
|
||||
async function launch() {
|
||||
if (isShuttingDown) {
|
||||
console.log("Shutdown in progress, waiting...");
|
||||
await new Promise(resolve => setTimeout(resolve, 1000));
|
||||
return launch(); // Retry after delay
|
||||
}
|
||||
resetTimer();
|
||||
if (!driver) {
|
||||
console.log("Launching Firefox");
|
||||
const options = new firefox.Options();
|
||||
// Explicitly set headless mode and optimize for server
|
||||
options.addArguments('--headless', '--no-sandbox', '--disable-gpu');
|
||||
// Limit content processes to reduce resource usage
|
||||
options.setPreference('dom.ipc.processCount', 1);
|
||||
|
||||
const service = new firefox.ServiceBuilder(geckodriverPath);
|
||||
driver = await new Builder()
|
||||
.forBrowser('firefox')
|
||||
.setFirefoxService(new firefox.ServiceBuilder(geckodriverPath))
|
||||
.setFirefoxOptions(options.headless())
|
||||
.setFirefoxService(service)
|
||||
.setFirefoxOptions(options)
|
||||
.build();
|
||||
}
|
||||
}
|
||||
|
||||
async function shutdown () {
|
||||
if (driver) {
|
||||
async function shutdown() {
|
||||
if (driver && !isShuttingDown) {
|
||||
isShuttingDown = true;
|
||||
console.log("Shutting down Firefox");
|
||||
// This is an attempt at avoiding a race condition if someone
|
||||
// makes a call and resets the timer while the shutdown is in
|
||||
// progress.
|
||||
const d = driver;
|
||||
driver = null;
|
||||
await d.quit();
|
||||
try {
|
||||
const d = driver;
|
||||
driver = null;
|
||||
await d.quit();
|
||||
// Explicitly stop the service
|
||||
const service = d.service;
|
||||
if (service) {
|
||||
service.stop();
|
||||
}
|
||||
console.log("Firefox shutdown complete");
|
||||
} catch (error) {
|
||||
console.error("Error during shutdown:", error);
|
||||
// Forcefully kill lingering processes (Linux/Unix)
|
||||
try {
|
||||
execSync('pkill -u $USER firefox || true');
|
||||
execSync('pkill -u $USER geckodriver || true');
|
||||
console.log("Terminated lingering Firefox/GeckoDriver processes");
|
||||
} catch (killError) {
|
||||
console.error("Error killing processes:", killError);
|
||||
}
|
||||
} finally {
|
||||
isShuttingDown = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function url2pdf (url) {
|
||||
async function url2pdf(url) {
|
||||
await launch();
|
||||
await driver.get(url);
|
||||
return await driver.printPage({width: 21.0, height: 29.7});
|
||||
try {
|
||||
console.log(`Navigating to ${url}`);
|
||||
await driver.get(url);
|
||||
// Add delay to stabilize Marionette communication
|
||||
await driver.sleep(3000);
|
||||
const pdf = await driver.printPage({ width: 21.0, height: 29.7 });
|
||||
resetTimer(); // Reset timer after successful operation
|
||||
return pdf;
|
||||
} catch (error) {
|
||||
console.error("Error in url2pdf:", error);
|
||||
await shutdown(); // Force shutdown on error
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
// Periodically clean up orphaned processes (every 5 minutes)
|
||||
setInterval(() => {
|
||||
try {
|
||||
const firefoxCount = execSync('pgrep -c firefox || echo 0').toString().trim();
|
||||
if (parseInt(firefoxCount) > 0 && !driver) {
|
||||
console.log(`Found ${firefoxCount} orphaned Firefox processes, cleaning up...`);
|
||||
execSync('pkill -u $USER firefox || true');
|
||||
execSync('pkill -u $USER geckodriver || true');
|
||||
console.log("Cleaned up orphaned processes");
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error checking orphaned processes:", error);
|
||||
}
|
||||
}, 300000);
|
||||
|
||||
module.exports = { url2pdf };
|
||||
|
||||
@@ -29,6 +29,7 @@
|
||||
"@dougal/binary": "file:../../modules/@dougal/binary",
|
||||
"@dougal/organisations": "file:../../modules/@dougal/organisations",
|
||||
"@dougal/user": "file:../../modules/@dougal/user",
|
||||
"async": "^3.2.6",
|
||||
"body-parser": "gitlab:aaltronav/contrib/expressjs/body-parser",
|
||||
"busboy": "^1.6.0",
|
||||
"compression": "^1.8.1",
|
||||
|
||||
6
package-lock.json
generated
6
package-lock.json
generated
@@ -9359,6 +9359,7 @@
|
||||
"@dougal/binary": "file:../../modules/@dougal/binary",
|
||||
"@dougal/organisations": "file:../../modules/@dougal/organisations",
|
||||
"@dougal/user": "file:../../modules/@dougal/user",
|
||||
"async": "^3.2.6",
|
||||
"body-parser": "gitlab:aaltronav/contrib/expressjs/body-parser",
|
||||
"busboy": "^1.6.0",
|
||||
"compression": "^1.8.1",
|
||||
@@ -14171,6 +14172,11 @@
|
||||
"node": ">=0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/async": {
|
||||
"version": "3.2.6",
|
||||
"resolved": "https://registry.npmjs.org/async/-/async-3.2.6.tgz",
|
||||
"integrity": "sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA=="
|
||||
},
|
||||
"node_modules/asynckit": {
|
||||
"version": "0.4.0",
|
||||
"resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz",
|
||||
|
||||
Reference in New Issue
Block a user