mirror of
https://gitlab.com/wgp/dougal/software.git
synced 2025-12-06 13:07:08 +00:00
Compare commits
3 Commits
12a762f44f
...
8e4e70cbdc
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8e4e70cbdc | ||
|
|
4dadffbbe7 | ||
|
|
24dcebd0d9 |
@@ -2,6 +2,7 @@
|
|||||||
<v-dialog
|
<v-dialog
|
||||||
v-model="dialog"
|
v-model="dialog"
|
||||||
max-width="500"
|
max-width="500"
|
||||||
|
scrollable
|
||||||
style="z-index:2020;"
|
style="z-index:2020;"
|
||||||
>
|
>
|
||||||
<template v-slot:activator="{ on, attrs }">
|
<template v-slot:activator="{ on, attrs }">
|
||||||
@@ -58,6 +59,9 @@
|
|||||||
|
|
||||||
|
|
||||||
</v-window-item>
|
</v-window-item>
|
||||||
|
<v-window-item value="serverinfo">
|
||||||
|
<dougal-server-status :status="serverStatus"></dougal-server-status>
|
||||||
|
</v-window-item>
|
||||||
</v-window>
|
</v-window>
|
||||||
|
|
||||||
<v-divider></v-divider>
|
<v-divider></v-divider>
|
||||||
@@ -69,8 +73,7 @@
|
|||||||
text
|
text
|
||||||
:href="`mailto:${email}?Subject=Question`"
|
:href="`mailto:${email}?Subject=Question`"
|
||||||
>
|
>
|
||||||
<v-icon class="d-lg-none">mdi-help-circle</v-icon>
|
<v-icon title="Ask a question">mdi-help-circle</v-icon>
|
||||||
<span class="d-none d-lg-inline">Ask a question</span>
|
|
||||||
</v-btn>
|
</v-btn>
|
||||||
|
|
||||||
<v-btn
|
<v-btn
|
||||||
@@ -78,8 +81,7 @@
|
|||||||
text
|
text
|
||||||
href="mailto:dougal-support@aaltronav.eu?Subject=Bug report"
|
href="mailto:dougal-support@aaltronav.eu?Subject=Bug report"
|
||||||
>
|
>
|
||||||
<v-icon class="d-lg-none">mdi-bug</v-icon>
|
<v-icon title="Report a bug">mdi-bug</v-icon>
|
||||||
<span class="d-none d-lg-inline">Report a bug</span>
|
|
||||||
</v-btn>
|
</v-btn>
|
||||||
|
|
||||||
<!---
|
<!---
|
||||||
@@ -93,16 +95,36 @@
|
|||||||
</v-btn>
|
</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"
|
<v-btn v-if="versionHistory"
|
||||||
color="info"
|
color="info"
|
||||||
text
|
text
|
||||||
:title="page == 'support' ? 'View release notes' : 'View support info'"
|
title="View release notes"
|
||||||
:input-value="page == 'changelog'"
|
:input-value="page == 'changelog'"
|
||||||
@click="page = page == 'support' ? 'changelog' : 'support'"
|
@click="page = 'changelog'"
|
||||||
>
|
>
|
||||||
<v-icon>mdi-history</v-icon>
|
<v-icon>mdi-history</v-icon>
|
||||||
</v-btn>
|
</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>
|
<v-spacer></v-spacer>
|
||||||
|
|
||||||
|
|
||||||
@@ -124,46 +146,110 @@
|
|||||||
|
|
||||||
<script>
|
<script>
|
||||||
import { mapActions, mapGetters } from 'vuex';
|
import { mapActions, mapGetters } from 'vuex';
|
||||||
|
import DougalServerStatus from './server-status';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'DougalHelpDialog',
|
name: 'DougalHelpDialog',
|
||||||
|
|
||||||
|
components: {
|
||||||
|
DougalServerStatus
|
||||||
|
},
|
||||||
|
|
||||||
data () {
|
data () {
|
||||||
return {
|
return {
|
||||||
dialog: false,
|
dialog: false,
|
||||||
email: "dougal-support@aaltronav.eu",
|
email: "dougal-support@aaltronav.eu",
|
||||||
feed: btoa(encodeURIComponent("https://gitlab.com/wgp/dougal/software.atom?feed_token=XSPpvsYEny8YmH75Nz5W")),
|
feed: btoa(encodeURIComponent("https://gitlab.com/wgp/dougal/software.atom?feed_token=XSPpvsYEny8YmH75Nz5W")),
|
||||||
|
serverStatus: null,
|
||||||
clientVersion: process.env.DOUGAL_FRONTEND_VERSION ?? "(unknown)",
|
clientVersion: process.env.DOUGAL_FRONTEND_VERSION ?? "(unknown)",
|
||||||
serverVersion: null,
|
serverVersion: null,
|
||||||
versionHistory: null,
|
versionHistory: null,
|
||||||
releaseHistory: [],
|
releaseHistory: [],
|
||||||
releaseShown: null,
|
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: {
|
methods: {
|
||||||
async getServerVersion () {
|
async getServerVersion () {
|
||||||
if (!this.serverVersion) {
|
if (!this.serverVersion) {
|
||||||
const version = await this.api(['/version', {}, null, {silent:true}]);
|
const version = await this.api(['/version', {}, null, {silent:true}]);
|
||||||
this.serverVersion = version?.tag ?? "(unknown)";
|
this.serverVersion = version?.tag ?? "(unknown)";
|
||||||
|
if (version) this.lastUpdate = Date.now();
|
||||||
}
|
}
|
||||||
if (!this.versionHistory) {
|
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.releaseHistory = history;
|
||||||
this.versionHistory = history?.[this.serverVersion.replace(/-.*$/, "")] ?? null;
|
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"])
|
...mapActions(["api"])
|
||||||
},
|
},
|
||||||
|
|
||||||
async mounted () {
|
async mounted () {
|
||||||
this.getServerVersion();
|
this.getServerVersion();
|
||||||
|
this.getServerStatus();
|
||||||
},
|
},
|
||||||
|
|
||||||
async beforeUpdate () {
|
beforeDestroy() {
|
||||||
this.getServerVersion();
|
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>
|
||||||
@@ -16,7 +16,6 @@ module.exports = async function (req, res, next) {
|
|||||||
|
|
||||||
if (json.length) {
|
if (json.length) {
|
||||||
const data = bundle(json, {type});
|
const data = bundle(json, {type});
|
||||||
console.log("bundle", data);
|
|
||||||
res.status(200).send(Buffer.from(data));
|
res.status(200).send(Buffer.from(data));
|
||||||
} else {
|
} else {
|
||||||
res.status(404).send();
|
res.status(404).send();
|
||||||
|
|||||||
@@ -8,8 +8,6 @@ function bundle (json, opts = {}) {
|
|||||||
const deltas = [];
|
const deltas = [];
|
||||||
const values = [];
|
const values = [];
|
||||||
|
|
||||||
// console.log("JSON LENGTH", json.length);
|
|
||||||
// console.log("OPTS", geometries, payload);
|
|
||||||
|
|
||||||
if (type == 0) {
|
if (type == 0) {
|
||||||
/* Preplot information – sail line points
|
/* Preplot information – sail line points
|
||||||
@@ -74,7 +72,6 @@ function bundle (json, opts = {}) {
|
|||||||
type: Uint16Array
|
type: Uint16Array
|
||||||
});
|
});
|
||||||
|
|
||||||
console.log("JSON", json[0]);
|
|
||||||
return encode.sequential(json, el => el.line, el => el.point, deltas, values, type)
|
return encode.sequential(json, el => el.line, el => el.point, deltas, values, type)
|
||||||
|
|
||||||
} else if (type == 2) {
|
} else if (type == 2) {
|
||||||
@@ -222,9 +219,6 @@ function bundle (json, opts = {}) {
|
|||||||
type: Uint8Array
|
type: Uint8Array
|
||||||
});
|
});
|
||||||
|
|
||||||
console.log("DELTAS", deltas);
|
|
||||||
console.log("VALUES", values);
|
|
||||||
|
|
||||||
return encode.sequential(json, el => el.sequence, el => el.point, deltas, values, type)
|
return encode.sequential(json, el => el.sequence, el => el.point, deltas, values, type)
|
||||||
} else if (type == 3) {
|
} else if (type == 3) {
|
||||||
/* Final positions and raw vs final errors:
|
/* Final positions and raw vs final errors:
|
||||||
|
|||||||
@@ -1,52 +1,110 @@
|
|||||||
// TODO Append location to PATH
|
|
||||||
|
|
||||||
const path = require('path');
|
const path = require('path');
|
||||||
const fs = require('fs');
|
const fs = require('fs');
|
||||||
const { Builder, By, Key, until } = require('selenium-webdriver');
|
const { Builder, By, Key, until } = require('selenium-webdriver');
|
||||||
const firefox = require('selenium-webdriver/firefox');
|
const firefox = require('selenium-webdriver/firefox');
|
||||||
|
const { execSync } = require('child_process');
|
||||||
|
|
||||||
const geckodriverPath = path.resolve(__dirname, "geckodriver");
|
const geckodriverPath = path.resolve(__dirname, "geckodriver");
|
||||||
|
|
||||||
// We launch a browser instance and then start an activity timer.
|
// State to prevent race conditions
|
||||||
// We shut down the browser after a period of inactivity, to
|
|
||||||
// save memory.
|
|
||||||
let driver = null;
|
let driver = null;
|
||||||
let timer = null;
|
let timer = null;
|
||||||
|
let isShuttingDown = false;
|
||||||
|
|
||||||
|
// Verify GeckoDriver exists
|
||||||
|
if (!fs.existsSync(geckodriverPath)) {
|
||||||
|
throw new Error(`GeckoDriver not found at ${geckodriverPath}`);
|
||||||
|
}
|
||||||
|
|
||||||
function resetTimer() {
|
function resetTimer() {
|
||||||
clearTimeout(timer);
|
clearTimeout(timer);
|
||||||
timer = setTimeout(shutdown, 120000); // Yup, hardcoded to two minutes. For now anyway
|
timer = setTimeout(shutdown, 120000); // 2 minutes inactivity timeout
|
||||||
}
|
}
|
||||||
|
|
||||||
async function launch() {
|
async function launch() {
|
||||||
|
if (isShuttingDown) {
|
||||||
|
console.log("Shutdown in progress, waiting...");
|
||||||
|
await new Promise(resolve => setTimeout(resolve, 1000));
|
||||||
|
return launch(); // Retry after delay
|
||||||
|
}
|
||||||
resetTimer();
|
resetTimer();
|
||||||
if (!driver) {
|
if (!driver) {
|
||||||
console.log("Launching Firefox");
|
console.log("Launching Firefox");
|
||||||
const options = new firefox.Options();
|
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()
|
driver = await new Builder()
|
||||||
.forBrowser('firefox')
|
.forBrowser('firefox')
|
||||||
.setFirefoxService(new firefox.ServiceBuilder(geckodriverPath))
|
.setFirefoxService(service)
|
||||||
.setFirefoxOptions(options.headless())
|
.setFirefoxOptions(options)
|
||||||
.build();
|
.build();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function shutdown() {
|
async function shutdown() {
|
||||||
if (driver) {
|
if (driver && !isShuttingDown) {
|
||||||
|
isShuttingDown = true;
|
||||||
console.log("Shutting down Firefox");
|
console.log("Shutting down Firefox");
|
||||||
// This is an attempt at avoiding a race condition if someone
|
try {
|
||||||
// makes a call and resets the timer while the shutdown is in
|
|
||||||
// progress.
|
|
||||||
const d = driver;
|
const d = driver;
|
||||||
driver = null;
|
driver = null;
|
||||||
await d.quit();
|
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 launch();
|
||||||
|
try {
|
||||||
|
console.log(`Navigating to ${url}`);
|
||||||
await driver.get(url);
|
await driver.get(url);
|
||||||
return await driver.printPage({width: 21.0, height: 29.7});
|
// 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 };
|
module.exports = { url2pdf };
|
||||||
|
|||||||
Reference in New Issue
Block a user