Make cgi non-blocking and borrow more ideas from muSRFlow
This commit is contained in:
1 parent
0f7ca5d7a7
commit
737666cea7
7 files changed
+257
-86
No files matched your search
@@ -247,18 +247,33 @@ The script expects the TRIM.SP-NL engine to be installed already and installs:
|
||||
/var/www/cgi-bin/singleTrimSP.cgi CGI runner
|
||||
```
|
||||
|
||||
If the Plotly browser bundle is not available locally (under `node_modules`),
|
||||
the script downloads it from a CDN using `curl`. Override the download URL with
|
||||
`TRIMSP_PLOTLY_CDN` if you need a different mirror or version.
|
||||
|
||||
By default, the CGI uses the existing executable at
|
||||
`/usr/local/bin/trimspNL`. Set `TRIMSP_BIN_PATH` if it is installed elsewhere.
|
||||
|
||||
When `node` is available, the installed quick-start help page is stamped with
|
||||
the release version from `package.json`; without it, the page renders
|
||||
`development build` in its About section.
|
||||
|
||||
Deployment paths and ownership can be changed through environment variables:
|
||||
|
||||
| Variable | Default | Purpose |
|
||||
| --- | --- | --- |
|
||||
| `TRIMSP_WEB_TARGET` | `/var/www/html/TRIMSP-NL` | Browser asset directory |
|
||||
| `TRIMSP_CGI_TARGET` | `/var/www/cgi-bin/singleTrimSP.cgi` | Installed CGI path |
|
||||
| `TRIMSP_WEB_OWNER` | `apache` | Web file owner |
|
||||
| `TRIMSP_WEB_GROUP` | `apache` | Web file group |
|
||||
| `TRIMSP_WEB_OWNER` | *auto-detected* | Web file owner |
|
||||
| `TRIMSP_WEB_GROUP` | *auto-detected* | Web file group |
|
||||
| `TRIMSP_BIN_PATH` | `/usr/local/bin/trimspNL` | Existing engine executable used by the CGI |
|
||||
| `TRIMSP_PLOTLY_CDN` | `https://cdn.jsdelivr.net/npm/plotly.js-dist@1.58.5/plotly.js` | Plotly bundle download URL when unavailable locally |
|
||||
|
||||
Ownership is applied automatically. When the deployed tree is already owned by
|
||||
the invoking user (the common developer-machine case) the files are left as
|
||||
they are; otherwise they are handed to a web-server account, preferring
|
||||
`apache`, `www-data`, then `httpd`. Set `TRIMSP_WEB_OWNER` and
|
||||
`TRIMSP_WEB_GROUP` to force a specific owner.
|
||||
|
||||
For example, to use an engine installed in `/opt/trimsp/bin`:
|
||||
|
||||
|
||||
@@ -129,6 +129,28 @@ function readAsciiFile(filename) {
|
||||
}
|
||||
return(content);
|
||||
}
|
||||
|
||||
// Async counterparts used by the shared scan pipeline. Local disk IO is fast,
|
||||
// but returning promises keeps the call sites identical across web and Electron.
|
||||
async function writeAsciiFileAsync(filename,content) {
|
||||
try {
|
||||
await fs.promises.writeFile(filename, content, 'utf-8');
|
||||
} catch(e) {
|
||||
alert('Failed to save '+filename);
|
||||
return(0);
|
||||
}
|
||||
return(1);
|
||||
}
|
||||
|
||||
async function readAsciiFileAsync(filename) {
|
||||
if (Array.isArray(filename)) {filename=filename[0];}
|
||||
try {
|
||||
return await fs.promises.readFile(filename, 'utf8');
|
||||
} catch(e) {
|
||||
alert('Failed to read '+filename);
|
||||
return(0);
|
||||
}
|
||||
}
|
||||
|
||||
function fileExists(filename) {
|
||||
if (typeof filename !== 'string' || filename.length === 0) {
|
||||
|
||||
+80
-49
@@ -3376,18 +3376,20 @@ function adjust_scans()
|
||||
var scanRange = document.getElementById("ScansTable");
|
||||
var scanList = document.getElementById("ScansLine");
|
||||
|
||||
// Default to half the available local worker slots, rounded down, while
|
||||
// keeping at least one process. Web mode keeps the default at 1 because
|
||||
// the CGI backend still executes scan points sequentially.
|
||||
// Default to half the available worker slots, rounded down, while keeping
|
||||
// at least one process. Web mode runs scan points in parallel through
|
||||
// concurrent CGI requests, bounded by TRIMSP_WEB_MAX_CONCURRENCY.
|
||||
if (scanThreads) {
|
||||
let availableThreads = 1;
|
||||
if (!amIWeb() && typeof getParallelJobCount === 'function') {
|
||||
if (amIWeb()) {
|
||||
availableThreads = Math.max(1, TRIMSP_WEB_MAX_CONCURRENCY);
|
||||
} else if (typeof getParallelJobCount === 'function') {
|
||||
availableThreads = Math.max(1, getParallelJobCount());
|
||||
}
|
||||
scanThreads.max = availableThreads;
|
||||
scanThreads.disabled = amIWeb();
|
||||
scanThreads.disabled = false;
|
||||
scanThreads.title = amIWeb()
|
||||
? 'The web backend currently runs scan points sequentially.'
|
||||
? `The web backend runs up to ${availableThreads} scan points in parallel.`
|
||||
: `This computer provides up to ${availableThreads} worker slots.`;
|
||||
|
||||
if (scanThreads.value === "" || Number(scanThreads.value) < 1) {
|
||||
@@ -3735,6 +3737,12 @@ function yieldToRenderer() {
|
||||
return new Promise((resolve) => setTimeout(resolve, 0));
|
||||
}
|
||||
|
||||
// Upper bound on concurrent scan points in the browser backend. Each job is a
|
||||
// separate CGI request that runs its own TRIM.SP-NL process, so this keeps a
|
||||
// large scan from overwhelming the web server while still running several
|
||||
// points in parallel.
|
||||
var TRIMSP_WEB_MAX_CONCURRENCY = 4;
|
||||
|
||||
// Format the live profile legend from the scan parameter currently being run.
|
||||
function formatScanLegend(scanName, scanValue) {
|
||||
if (scanName === "E") {
|
||||
@@ -3842,7 +3850,7 @@ async function startSequence() {
|
||||
updateStatusBar({ backend: 'Web backend' });
|
||||
}
|
||||
updateStatusBar({ workPath: All["workPath"], run: 'Starting run...' });
|
||||
if (!writeAsciiFile(All["workPath"] + "/TrimSP.cfg", prep_cfg(0))) {
|
||||
if (!await writeAsciiFileAsync(All["workPath"] + "/TrimSP.cfg", prep_cfg(0))) {
|
||||
return(0);
|
||||
}
|
||||
|
||||
@@ -3929,7 +3937,7 @@ async function startSequence() {
|
||||
// Read back the outputs of one completed run and convert them into the
|
||||
// normalized data structures used by the live plots.
|
||||
async function collectRunResult(job) {
|
||||
let [cols,data]= readDatFile(All["workPath"]+"/"+job.filename+".rge");
|
||||
let [cols,data]= await readDatFileAsync(All["workPath"]+"/"+job.filename+".rge");
|
||||
let depth=data[0];
|
||||
let nmuons=data[1];
|
||||
let dz = (depth[1]-depth[0])/10;
|
||||
@@ -3939,7 +3947,7 @@ async function startSequence() {
|
||||
nmuons[i]=nmuons[i]/norm;
|
||||
}
|
||||
let seqRunFile = All["workPath"]+"/"+job.filename+".seq";
|
||||
let seqRunLines = readAsciiFile(seqRunFile).trim().split('\n');
|
||||
let seqRunLines = (await readAsciiFileAsync(seqRunFile)).trim().split('\n');
|
||||
return {
|
||||
index: job.index,
|
||||
scanValue: job.scanValue,
|
||||
@@ -3961,13 +3969,17 @@ async function startSequence() {
|
||||
await yieldToRenderer();
|
||||
}
|
||||
|
||||
// Web mode stays sequential because the CGI path still submits one job at
|
||||
// a time. Local/Electron mode can use a bounded worker pool.
|
||||
// Web jobs are executed by the CGI as a side effect of writing the *.inp
|
||||
// file, so they only need the upload. Local jobs also need an explicit
|
||||
// engine launch, which the pool performs with executeFileAsync.
|
||||
let rngType = initialAll["rngType"] || "pcg32";
|
||||
const canRunPool = jobs.length > 1 && (webOrApp || typeof executeFileAsync === 'function');
|
||||
|
||||
if (webOrApp || jobs.length <= 1 || typeof executeFileAsync !== 'function') {
|
||||
if (!canRunPool) {
|
||||
// Sequential path for a single job, or the local fallback when no async
|
||||
// executor exists. Local single runs keep inherited stdio for the log.
|
||||
for (const job of jobs) {
|
||||
if (!writeAsciiFile(All["workPath"]+"/"+job.filename+".inp", job.inputFileContent)) {
|
||||
if (!await writeAsciiFileAsync(All["workPath"]+"/"+job.filename+".inp", job.inputFileContent)) {
|
||||
return(0);
|
||||
}
|
||||
if (!webOrApp) {
|
||||
@@ -3980,9 +3992,12 @@ async function startSequence() {
|
||||
await updateCompletedResults();
|
||||
}
|
||||
} else {
|
||||
// Keep a small pool of local TRIM.SP-NL processes running concurrently. Jobs
|
||||
// are inserted into the final result set by scan index as they complete.
|
||||
let availableThreads = Math.max(1, getParallelJobCount());
|
||||
// Bounded worker pool. Web jobs run server-side through the CGI; local jobs
|
||||
// use executeFileAsync. Jobs are inserted into the final result set by scan
|
||||
// index as they complete, so out-of-order finishes do not affect the plots.
|
||||
let availableThreads = webOrApp
|
||||
? TRIMSP_WEB_MAX_CONCURRENCY
|
||||
: Math.max(1, getParallelJobCount());
|
||||
let requestedThreads = parseInt(initialAll["scanThreads"], 10);
|
||||
if (Number.isNaN(requestedThreads) || requestedThreads < 1) {
|
||||
requestedThreads = Math.max(1, Math.floor(availableThreads / 2));
|
||||
@@ -3994,46 +4009,56 @@ async function startSequence() {
|
||||
let activeJobs = 0;
|
||||
let completedJobs = 0;
|
||||
|
||||
await new Promise((resolve, reject) => {
|
||||
function launchNextJobs() {
|
||||
while (activeJobs < maxConcurrent && nextJob < jobs.length) {
|
||||
const job = jobs[nextJob];
|
||||
nextJob += 1;
|
||||
activeJobs += 1;
|
||||
writeAsciiFile(All["workPath"]+"/"+job.filename+".inp", job.inputFileContent);
|
||||
updateStatusBar({
|
||||
run: `Running ${completedJobs}/${jobs.length} completed, ${activeJobs} active`
|
||||
});
|
||||
// Pass the selected RNG backend explicitly so sequential and
|
||||
// parallel runs use the same generator choice.
|
||||
executeFileAsync(trimBin, [job.filename, rngType], All["workPath"])
|
||||
.then(() => collectRunResult(job))
|
||||
.then(async (result) => {
|
||||
results[result.index] = result;
|
||||
activeJobs -= 1;
|
||||
completedJobs += 1;
|
||||
updateStatusBar({
|
||||
run: `Completed ${completedJobs}/${jobs.length}, ${activeJobs} active`
|
||||
});
|
||||
await updateCompletedResults();
|
||||
if (completedJobs === jobs.length) {
|
||||
resolve();
|
||||
return;
|
||||
}
|
||||
launchNextJobs();
|
||||
})
|
||||
.catch(reject);
|
||||
try {
|
||||
await new Promise((resolve, reject) => {
|
||||
function launchNextJobs() {
|
||||
while (activeJobs < maxConcurrent && nextJob < jobs.length) {
|
||||
const job = jobs[nextJob];
|
||||
nextJob += 1;
|
||||
activeJobs += 1;
|
||||
updateStatusBar({
|
||||
run: `Running ${completedJobs}/${jobs.length} completed, ${activeJobs} active`
|
||||
});
|
||||
// Pass the selected RNG backend explicitly so sequential and
|
||||
// parallel runs use the same generator choice.
|
||||
writeAsciiFileAsync(All["workPath"]+"/"+job.filename+".inp", job.inputFileContent)
|
||||
.then((written) => {
|
||||
if (!written) { throw new Error(`Could not submit ${job.filename}`); }
|
||||
if (webOrApp) { return; }
|
||||
return executeFileAsync(trimBin, [job.filename, rngType], All["workPath"]);
|
||||
})
|
||||
.then(() => collectRunResult(job))
|
||||
.then(async (result) => {
|
||||
results[result.index] = result;
|
||||
activeJobs -= 1;
|
||||
completedJobs += 1;
|
||||
updateStatusBar({
|
||||
run: `Completed ${completedJobs}/${jobs.length}, ${activeJobs} active`
|
||||
});
|
||||
await updateCompletedResults();
|
||||
if (completedJobs === jobs.length) {
|
||||
resolve();
|
||||
return;
|
||||
}
|
||||
launchNextJobs();
|
||||
})
|
||||
.catch(reject);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
launchNextJobs();
|
||||
});
|
||||
launchNextJobs();
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Scan job failed:', error);
|
||||
updateStatusBar({ run: 'Run failed' });
|
||||
return(0);
|
||||
}
|
||||
}
|
||||
|
||||
let combinedSeqLines = buildCombinedSequenceLines(results);
|
||||
let seq_file = All["workPath"]+"/"+All["fileNamePrefix"]+"_Seq_Results.dat";
|
||||
if (combinedSeqLines.length > 0) {
|
||||
if (!writeAsciiFile(seq_file, combinedSeqLines.join('\n') + '\n')) {
|
||||
if (!await writeAsciiFileAsync(seq_file, combinedSeqLines.join('\n') + '\n')) {
|
||||
return(0);
|
||||
}
|
||||
}
|
||||
@@ -4053,6 +4078,12 @@ function readDatFile(filename) {
|
||||
return parseDatContent(lines);
|
||||
}
|
||||
|
||||
async function readDatFileAsync(filename) {
|
||||
// Async counterpart used by the scan pipeline in both web and Electron.
|
||||
let lines = await readAsciiFileAsync(filename);
|
||||
return parseDatContent(lines);
|
||||
}
|
||||
|
||||
function parseDatContent(lines) {
|
||||
// Parse whitespace-separated column data from an in-memory string.
|
||||
let data = [];
|
||||
|
||||
+34
-19
@@ -152,10 +152,12 @@ function recordWebDiagnostic(filename, request, response) {
|
||||
}
|
||||
}
|
||||
|
||||
function writeAsciiFile(filename,content) {
|
||||
// Async file IO through the CGI. Using fetch instead of a synchronous
|
||||
// XMLHttpRequest keeps the browser responsive while the server runs the
|
||||
// simulation, and lets independent scan points run concurrently.
|
||||
async function writeAsciiFileAsync(filename, content) {
|
||||
// Write string content into ascii file filename
|
||||
// Go via CGI script
|
||||
//console.log(filename, content);
|
||||
// Prepare CGI args use POST for long files
|
||||
let cgiargs = new URLSearchParams();
|
||||
cgiargs.set("fn", filename);
|
||||
@@ -171,20 +173,27 @@ function writeAsciiFile(filename,content) {
|
||||
cgiargs.set("line" + i, lines[i].replace(/\s\s+/g, ' '));
|
||||
}
|
||||
|
||||
var xhttp;
|
||||
xhttp = new XMLHttpRequest();
|
||||
let request = "/cgi-bin/singleTrimSP.cgi"; //POST
|
||||
xhttp.open("POST", request, false); //POST
|
||||
|
||||
//Send the proper header information along with the request
|
||||
xhttp.setRequestHeader('Content-type', 'application/x-www-form-urlencoded'); //POST
|
||||
xhttp.send(cgiargs.toString()); //POST
|
||||
|
||||
let request = "/cgi-bin/singleTrimSP.cgi";
|
||||
let xhttp = { status: 0, statusText: "", responseText: "" };
|
||||
let response = null;
|
||||
try {
|
||||
response = JSON.parse(xhttp.responseText);
|
||||
let fetched = await fetch(request, {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/x-www-form-urlencoded" },
|
||||
body: cgiargs.toString(),
|
||||
cache: "no-store"
|
||||
});
|
||||
xhttp.status = fetched.status;
|
||||
xhttp.statusText = fetched.statusText;
|
||||
xhttp.responseText = await fetched.text();
|
||||
try {
|
||||
response = JSON.parse(xhttp.responseText);
|
||||
} catch (error) {
|
||||
console.error("Invalid CGI response", error, xhttp.responseText);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Invalid CGI response", error, xhttp.responseText);
|
||||
console.error("CGI request failed", error);
|
||||
xhttp.statusText = error && error.message ? error.message : "Network error";
|
||||
}
|
||||
recordWebDiagnostic(filename, xhttp, response);
|
||||
|
||||
@@ -200,11 +209,11 @@ function writeAsciiFile(filename,content) {
|
||||
outputLink.title = `Download ${prefix[2]}.tgz`;
|
||||
outputLink.hidden = false;
|
||||
}
|
||||
|
||||
|
||||
return(1);
|
||||
}
|
||||
|
||||
function readAsciiFile(filename) {
|
||||
async function readAsciiFileAsync(filename) {
|
||||
// Read server-side simulation output through the CGI. Files remain in
|
||||
// filesystem /tmp and do not need to be exposed directly by Apache.
|
||||
if (filename == "" || filename == undefined || filename == null) {
|
||||
@@ -213,10 +222,16 @@ function readAsciiFile(filename) {
|
||||
|
||||
let request = "/cgi-bin/singleTrimSP.cgi?action=read&fn="
|
||||
+ encodeURIComponent(filename) + "&_=" + Date.now();
|
||||
let xhttp = new XMLHttpRequest();
|
||||
xhttp.open("GET", request, false);
|
||||
xhttp.overrideMimeType("text/plain");
|
||||
xhttp.send();
|
||||
let xhttp = { status: 0, statusText: "", responseText: "" };
|
||||
try {
|
||||
let fetched = await fetch(request, { cache: "no-store" });
|
||||
xhttp.status = fetched.status;
|
||||
xhttp.statusText = fetched.statusText;
|
||||
xhttp.responseText = await fetched.text();
|
||||
} catch (error) {
|
||||
console.error("CGI read failed", error);
|
||||
xhttp.statusText = error && error.message ? error.message : "Network error";
|
||||
}
|
||||
|
||||
if (xhttp.status < 200 || xhttp.status >= 300) {
|
||||
recordWebDiagnostic(filename, xhttp, {
|
||||
|
||||
@@ -194,7 +194,7 @@ function showQuickStartWindow(sectionId = 'helpLayers') {
|
||||
if (quickStartWindow && !quickStartWindow.isDestroyed()) {
|
||||
quickStartWindow.show();
|
||||
quickStartWindow.focus();
|
||||
quickStartWindow.webContents.send('quickStartSection', { sectionId: sectionId });
|
||||
quickStartWindow.webContents.send('quickStartSection', { sectionId: sectionId, version: app.getVersion() });
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -213,7 +213,7 @@ function showQuickStartWindow(sectionId = 'helpLayers') {
|
||||
}
|
||||
});
|
||||
quickStartWindow.webContents.once('did-finish-load', () => {
|
||||
quickStartWindow.webContents.send('quickStartSection', { sectionId: sectionId });
|
||||
quickStartWindow.webContents.send('quickStartSection', { sectionId: sectionId, version: app.getVersion() });
|
||||
});
|
||||
quickStartWindow.loadFile('quick-start.html');
|
||||
quickStartWindow.on('closed', () => {
|
||||
|
||||
+20
-1
@@ -22,7 +22,7 @@
|
||||
<section class="quick-start-section" id="helpScans">
|
||||
<h3>Energy and parameter scans</h3>
|
||||
<p>Enable the scan, select its parameter, and choose either a regular range or an explicit set of values. The summary reports the number of points and workers before the run starts.</p>
|
||||
<p>Electron can run scan points in parallel. The thread setting is local to the computer and is intentionally not stored in configuration files or shared links. The web backend executes scan points sequentially.</p>
|
||||
<p>Scan points can run in parallel. The thread setting is local to the computer and is intentionally not stored in configuration files or shared links. In the web application the number of concurrent points is limited to protect the server.</p>
|
||||
</section>
|
||||
|
||||
<section class="quick-start-section" id="helpResults">
|
||||
@@ -42,6 +42,12 @@
|
||||
<p>The share button copies a link containing the current layers, projectile, scan, RNG, and advanced settings. Opening the link restores those inputs in the web application.</p>
|
||||
<p>Output folders, file prefixes, worker counts, logs, and generated data are never included. The recipient chooses their own output location before running.</p>
|
||||
</section>
|
||||
|
||||
<section class="quick-start-section" id="helpAbout">
|
||||
<h3>About TRIMSP-NL Workbench</h3>
|
||||
<p><b>TRIMSP-NL Workbench <span id="trimspVersion">__TRIMSP_VERSION__</span></b> is a desktop and web workflow for low-energy muon and ion implantation simulations, combining the TRIM.SP-NL transport engine with the GUI and scan workflow.</p>
|
||||
<p>Author: <a href="mailto:zaher.salman@psi.ch">Zaher Salman</a>. Copyright (C) 2026 Zaher Salman. GNU GPL version 3 or later. Source repository: <a href="https://gitea.psi.ch/LMU/TRIMSP" target="_blank" rel="noopener">gitea.psi.ch/LMU/TRIMSP</a>.</p>
|
||||
</section>
|
||||
</main>
|
||||
|
||||
<footer class="quick-start-footer" id="quickStartFooter">
|
||||
@@ -64,6 +70,18 @@
|
||||
}, 0);
|
||||
}
|
||||
|
||||
var versionSpan = document.getElementById('trimspVersion');
|
||||
function setVersion(version) {
|
||||
if (!versionSpan) return;
|
||||
if (version) {
|
||||
versionSpan.textContent = version;
|
||||
} else if (versionSpan.textContent.indexOf('__TRIMSP_VERSION__') !== -1) {
|
||||
versionSpan.textContent = 'development build';
|
||||
}
|
||||
}
|
||||
|
||||
window.focusQuickStartSection = focusSection;
|
||||
setVersion();
|
||||
focusSection(window.location.hash ? window.location.hash.slice(1) : 'helpLayers');
|
||||
window.addEventListener('hashchange', function() {
|
||||
focusSection(window.location.hash ? window.location.hash.slice(1) : 'helpLayers');
|
||||
@@ -73,6 +91,7 @@
|
||||
try {
|
||||
require('electron').ipcRenderer.on('quickStartSection', function(event, payload) {
|
||||
focusSection(payload && payload.sectionId ? payload.sectionId : 'helpLayers');
|
||||
setVersion(payload && payload.version);
|
||||
});
|
||||
} catch (error) {
|
||||
console.log('Could not attach Quick Start Electron channel:', error);
|
||||
|
||||
+82
-13
@@ -1,6 +1,6 @@
|
||||
#!/usr/bin/env bash
|
||||
# Copyright (C) 2026 Zaher Salman
|
||||
# SPDX-License-Identifier: GPL-2.0-only
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
@@ -8,9 +8,13 @@ project_root="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||
web_target="${TRIMSP_WEB_TARGET:-/var/www/html/TRIMSP-NL}"
|
||||
cgi_target="${TRIMSP_CGI_TARGET:-/var/www/cgi-bin/singleTrimSP.cgi}"
|
||||
trimsp_binary="${TRIMSP_BIN_PATH:-/usr/local/bin/trimspNL}"
|
||||
web_owner="${TRIMSP_WEB_OWNER:-apache}"
|
||||
web_group="${TRIMSP_WEB_GROUP:-apache}"
|
||||
plotly_source="$project_root/node_modules/plotly.js-dist/plotly.js"
|
||||
plotly_cdn="${TRIMSP_PLOTLY_CDN:-https://cdn.jsdelivr.net/npm/plotly.js-dist@1.58.5/plotly.js}"
|
||||
|
||||
# Staging files created by this script, removed on exit so a CDN download does
|
||||
# not leak into /tmp after an early failure.
|
||||
tmp_files=""
|
||||
trap 'rm -f $tmp_files' EXIT
|
||||
|
||||
if [[ ! -x "$trimsp_binary" ]]; then
|
||||
printf 'Installed TRIM.SP-NL binary is not executable: %s\n' "$trimsp_binary" >&2
|
||||
@@ -19,9 +23,22 @@ if [[ ! -x "$trimsp_binary" ]]; then
|
||||
fi
|
||||
|
||||
if [[ ! -f "$plotly_source" ]]; then
|
||||
printf 'Plotly browser bundle not found: %s\n' "$plotly_source" >&2
|
||||
printf 'Run npm install before deploying.\n' >&2
|
||||
exit 1
|
||||
printf 'Plotly browser bundle not found locally: %s\n' "$plotly_source" >&2
|
||||
printf 'Downloading bundle from CDN instead ...\n' >&2
|
||||
if ! command -v curl >/dev/null 2>&1; then
|
||||
printf 'curl is required to download the Plotly bundle.\n' >&2
|
||||
printf 'Install curl, run npm install, or set TRIMSP_PLOTLY_CDN.\n' >&2
|
||||
exit 1
|
||||
fi
|
||||
plotly_tmp="$(mktemp)"
|
||||
tmp_files="$tmp_files $plotly_tmp"
|
||||
if ! curl -fsSL "$plotly_cdn" -o "$plotly_tmp"; then
|
||||
printf 'Failed to download Plotly bundle from %s\n' "$plotly_cdn" >&2
|
||||
printf 'Run npm install before deploying.\n' >&2
|
||||
exit 1
|
||||
fi
|
||||
printf 'Downloaded Plotly bundle from %s\n' "$plotly_cdn"
|
||||
plotly_source="$plotly_tmp"
|
||||
fi
|
||||
|
||||
perl -c "$project_root/singleTrimSP.cgi" >/dev/null
|
||||
@@ -42,10 +59,26 @@ install -m 0644 \
|
||||
install -m 0644 "$plotly_source" "$web_target/plotly.js"
|
||||
ln -sfn TrimSP.html "$web_target/index.html"
|
||||
|
||||
# Brand the installed help page with the current release version, matching the
|
||||
# About dialog in the Electron edition. The source keeps a __TRIMSP_VERSION__
|
||||
# token that renders as "development build" when left unsubstituted; the
|
||||
# installed copy is rewritten with the version from package.json. Idempotent:
|
||||
# an already-branded file contains no token, so re-running matches nothing.
|
||||
if command -v node >/dev/null 2>&1; then
|
||||
version="$(node -p 'require(process.argv[1]).version' "$project_root/package.json")"
|
||||
tmp_qs="$(mktemp)"
|
||||
tmp_files="$tmp_files $tmp_qs"
|
||||
sed "s#__TRIMSP_VERSION__#$version#g" "$web_target/quick-start.html" > "$tmp_qs"
|
||||
install -m 0644 "$tmp_qs" "$web_target/quick-start.html"
|
||||
printf 'Branded %s with TRIMSP-NL version %s\n' "$web_target/quick-start.html" "$version"
|
||||
else
|
||||
printf 'node not found; quick-start.html keeps the source version token\n'
|
||||
fi
|
||||
|
||||
# Keep the repository CGI generic while embedding the selected installed
|
||||
# binary path in the deployed copy.
|
||||
cgi_stage="$(mktemp)"
|
||||
trap 'rm -f "$cgi_stage"' EXIT
|
||||
tmp_files="$tmp_files $cgi_stage"
|
||||
escaped_trimsp_binary="${trimsp_binary//\\/\\\\}"
|
||||
escaped_trimsp_binary="${escaped_trimsp_binary//&/\\&}"
|
||||
escaped_trimsp_binary="${escaped_trimsp_binary//|/\\|}"
|
||||
@@ -54,12 +87,43 @@ sed 's|^my \$trimsp_bin = .*|my $trimsp_bin = "'"$escaped_trimsp_binary"'";|' \
|
||||
"$project_root/singleTrimSP.cgi" > "$cgi_stage"
|
||||
install -m 0755 "$cgi_stage" "$cgi_target"
|
||||
|
||||
if getent passwd "$web_owner" >/dev/null 2>&1 && getent group "$web_group" >/dev/null 2>&1; then
|
||||
chown -R "$web_owner:$web_group" "$web_target"
|
||||
# The browser assets and the CGI must be readable (and, for the CGI,
|
||||
# executable) by the web server. On the live server the web space is owned by
|
||||
# the apache user; on a developer machine the files may instead be owned by
|
||||
# the deploying user and served straight from a user-owned tree. Determine the
|
||||
# owner to hand the files to, preferring an explicit override, then a real
|
||||
# web-server account, and keeping the current owner when it is already the
|
||||
# deploying user (the common developer-machine case, where the deploying user
|
||||
# simply owns the web space and chowning to a nonexistent apache user would
|
||||
# fail).
|
||||
web_owner="${TRIMSP_WEB_OWNER:-}"
|
||||
web_group="${TRIMSP_WEB_GROUP:-$web_owner}"
|
||||
|
||||
if [ -z "$web_owner" ]; then
|
||||
# Detect from the target filesystem: if the deployed tree is already owned
|
||||
# by the invoking user, leave ownership untouched.
|
||||
if [ "$(stat -c %u "$web_target")" = "$(id -u)" ]; then
|
||||
web_owner=""
|
||||
web_group=""
|
||||
else
|
||||
# Prefer a real web-server account when one exists.
|
||||
for cand in apache www-data httpd; do
|
||||
if id "$cand" >/dev/null 2>&1; then
|
||||
web_owner="$cand"
|
||||
web_group="$cand"
|
||||
break
|
||||
fi
|
||||
done
|
||||
# With no matching account, fall back to the invoking user (the trees
|
||||
# were written by this deploy, so they are already owned by them).
|
||||
[ -z "$web_owner" ] && web_owner="$(id -un)" && web_group="$(id -gn)"
|
||||
fi
|
||||
fi
|
||||
|
||||
if [ -n "$web_owner" ]; then
|
||||
chown -R "$web_owner:$web_group" "$web_target/"
|
||||
chown "$web_owner:$web_group" "$cgi_target"
|
||||
else
|
||||
printf 'Warning: owner/group %s:%s does not exist; ownership was not changed.\n' \
|
||||
"$web_owner" "$web_group" >&2
|
||||
chmod -R u+rX "$web_target/" 2>/dev/null || true
|
||||
fi
|
||||
|
||||
if command -v restorecon >/dev/null 2>&1; then
|
||||
@@ -69,7 +133,12 @@ fi
|
||||
printf 'Installed TRIMSP-NL web assets in %s\n' "$web_target"
|
||||
printf 'Installed TRIMSP-NL CGI runner at %s\n' "$cgi_target"
|
||||
printf 'Using installed TRIM.SP-NL engine at %s\n' "$trimsp_binary"
|
||||
if [ -n "$web_owner" ]; then
|
||||
printf 'Ownership set to %s:%s\n' "$web_owner" "$web_group"
|
||||
else
|
||||
printf 'Ownership left unchanged (deployed by the owning user)\n'
|
||||
fi
|
||||
printf '\nThe web server must provide:\n'
|
||||
printf ' /cgi-bin/singleTrimSP.cgi -> %s\n' "$cgi_target"
|
||||
printf 'For SELinux deployments, verify CGI execution and write access with:\n'
|
||||
printf ' ausearch -m avc -ts recent\n'
|
||||
printf ' ausearch -m avc -ts recent\n'
|
||||
Reference in new issue
Block a user