diff --git a/CITATION.cff b/CITATION.cff index 9820cea..d0d8af3 100644 --- a/CITATION.cff +++ b/CITATION.cff @@ -2,8 +2,8 @@ cff-version: 1.2.0 message: "If you use this software, please cite the TRIMSP-NL Workbench publication and this software release." title: "TRIMSP-NL Workbench" type: software -version: 1.4.0 -date-released: 2026-08-27 +version: 1.5.0 +date-released: 2026-08-28 url: "http://musruser.psi.ch/TRIMSP-NL/" repository-code: "https://gitea.psi.ch/LMU/TRIMSP" license: GPL-3.0-or-later diff --git a/README.md b/README.md index 36e5aad..7849bfb 100644 --- a/README.md +++ b/README.md @@ -118,6 +118,11 @@ npm start Note: you do not need to run `npm install` every time. Next time you can simply run `npm start` only. +Use the `?` button for contextual Quick Start help. The adjacent share button +copies a link containing the current scientific configuration for the web +application. Shared links include layers, projectile, scan, RNG, and advanced +settings, but exclude local paths, file prefixes, worker counts, and results. + ###### Building RPM and DEB packages ###### Install [Node.js] (`nodejs`, `nodejs-libs` and `npm`), plus the Fortran toolchain @@ -228,24 +233,56 @@ which matches how the packaged app currently handles simulation input/output. ###### Deploying on a web server ###### -The browser version requires a server-side setup in addition to the static -HTML/JavaScript files. +The repository includes the browser assets, CGI runner, and a deployment +script for an Apache-style server. With the default settings, run: -In particular: +```bash +sudo ./scripts/deploy-web.sh +``` -* the browser build uses `TrimSPweb.js` rather than the Electron-specific - `TrimSPelec.js` -* the simulation backend must be available on the server -* a web-exposed helper/CGI layer is required to create input files, run the - backend, and return the generated output +The script expects the TRIM.SP-NL engine to be installed already and installs: -Notes: +```text +/var/www/html/TRIMSP-NL/ browser application +/var/www/cgi-bin/singleTrimSP.cgi CGI runner +``` -* this repository does not currently include the CGI/backend helper used by - the online deployment -* a plain static web server is therefore not sufficient for running web - simulations from this checkout -* static hosting can still be used to test page loading only +By default, the CGI uses the existing executable at +`/usr/local/bin/trimspNL`. Set `TRIMSP_BIN_PATH` if it is installed elsewhere. + +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_BIN_PATH` | `/usr/local/bin/trimspNL` | Existing engine executable used by the CGI | + +For example, to use an engine installed in `/opt/trimsp/bin`: + +```bash +sudo TRIMSP_BIN_PATH=/opt/trimsp/bin/trimspNL \ + ./scripts/deploy-web.sh +``` + +The web server must execute the CGI through `/cgi-bin/singleTrimSP.cgi`. The +CGI keeps simulation files in filesystem `/tmp` and serves only requested run +outputs and archives; `/tmp` does not need to be exposed through the web +server. On SELinux systems, inspect denied operations with: + +```bash +ausearch -m avc -ts recent +``` + +The Apache account needs permission to execute the CGI and engine and to create +the per-run directories in `/tmp`. + +After a web run, the status bar provides the results archive through +**Outputs** and shows CGI status plus captured TRIM.SP-NL output through +**Details**. Diagnostic output is kept in the browser session only and is not +included in the `.tgz` archive. ### Contact ### diff --git a/TrimSP.html b/TrimSP.html index 1d11d41..7fc8566 100644 --- a/TrimSP.html +++ b/TrimSP.html @@ -2,8 +2,8 @@ + - - TRIMSP-NL Workbench - -
-
-
- - - - +
+
+ +
- - - - - -
- - - - - - - - - - - - - - +
+ +
+ +
+ + + +
+
+
+
+
+
+
+ Target +

Layer stack

+
+ +
+
+
- - -
- - - -
- - -
Layer #CompositionDensity [g/cm3]Thickness [Å]
-
+
LayerCompositionDensity [g/cm3]Thickness [Å]
-
- - - - - - - - - - - - - - - - - - - -
Projectile parameters
Projectile -
Number of projectiles
Starting depth [Å]
Depth increment [Å]
Energy [eV]
Energy sigma [eV]
Angle [deg]
Angle sigma [deg]
RNG / Seed - - -
-
+
+
+
+
+
+ Beam +

Projectile

+
+
+
+ + + + + + + + + + +
+
+
- Enable scan on - - - - - - - - - - + + +
+
+ + + +
+ + +
Single simulation using the Layers settings.
+
- - - - - -
+
+
+
+
+
+
+
+
- +
+
+
+
+ Transport model +

Advanced parameters

+
+
+
-
EF @@ -373,29 +403,43 @@
-

- For further details see:
- W. Eckstein, Computer Simulation of Ion-Solid Interactions,
- Springer Series in Materials Science, Vol. 10 (Springer-Verlag, Berlin, 1991).
- - https://doi.org/10.1007/978-3-642-73513-4 - -

-

- If you use this software in published work, please cite the relevant - TRIM.SP references and the TRIMSP-NL Workbench paper. -

-

- Source repository: - - gitea.psi.ch/LMU/TRIMSP - -

+
+ + + - - - + +
Folder: - Backend: - @@ -406,7 +450,29 @@
0%
+ + +
+
+ Web backend +

Run details

+
+ +
+

CGI responses and TRIM.SP-NL output for this browser session.

+
No web requests have been made yet.
+
+ +
+ TRIMSP-NL Quick Start + +
+ +
+ + diff --git a/scripts/deploy-web.sh b/scripts/deploy-web.sh new file mode 100755 index 0000000..b814e6b --- /dev/null +++ b/scripts/deploy-web.sh @@ -0,0 +1,75 @@ +#!/usr/bin/env bash +# Copyright (C) 2026 Zaher Salman +# SPDX-License-Identifier: GPL-2.0-only + +set -euo pipefail + +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" + +if [[ ! -x "$trimsp_binary" ]]; then + printf 'Installed TRIM.SP-NL binary is not executable: %s\n' "$trimsp_binary" >&2 + printf 'Install it first or set TRIMSP_BIN_PATH.\n' >&2 + exit 1 +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 +fi + +perl -c "$project_root/singleTrimSP.cgi" >/dev/null + +# Install only files used by the browser application. Electron-specific code, +# package metadata, tests, and Fortran sources stay outside the document root. +install -d -m 0755 "$web_target" "$(dirname "$cgi_target")" +install -m 0644 \ + "$project_root/TrimSP.html" \ + "$project_root/quick-start.html" \ + "$project_root/TrimSPlib.js" \ + "$project_root/TrimSPweb.js" \ + "$project_root/myplots.js" \ + "$project_root/ZGUI.css" \ + "$project_root/plotly.css" \ + "$project_root/icon.png" \ + "$web_target/" +install -m 0644 "$plotly_source" "$web_target/plotly.js" +ln -sfn TrimSP.html "$web_target/index.html" + +# 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 +escaped_trimsp_binary="${trimsp_binary//\\/\\\\}" +escaped_trimsp_binary="${escaped_trimsp_binary//&/\\&}" +escaped_trimsp_binary="${escaped_trimsp_binary//|/\\|}" +escaped_trimsp_binary="${escaped_trimsp_binary//\"/\\\"}" +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" + 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 +fi + +if command -v restorecon >/dev/null 2>&1; then + restorecon -RF "$web_target" "$cgi_target" || true +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" +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' diff --git a/singleTrimSP.cgi b/singleTrimSP.cgi new file mode 100644 index 0000000..eb6ee12 --- /dev/null +++ b/singleTrimSP.cgi @@ -0,0 +1,185 @@ +#!/usr/bin/perl +# +# Copyright Zaher Salman 2023-. +# zaher.salman@psi.ch + +use strict; +use warnings; +use CGI; +use File::Path qw(make_path remove_tree); +use File::Temp qw(tempfile); +use JSON::PP qw(encode_json); + +# The web installer rewrites this path to the selected installed binary. +my $trimsp_bin = "/usr/local/bin/trimspNL"; +my $workroot = "/tmp"; + +my $in = CGI->new; +my $action = $in->param("action") // "run"; + +if ($action eq "read") { + my $read_filename = $in->param("fn") // ""; + if ($read_filename !~ m{^/tmp/[A-Za-z0-9_-]+/[A-Za-z0-9_.-]+$} + || !-f $read_filename) { + print "Status: 404 Not Found\r\n"; + print "Content-Type: text/plain; charset=UTF-8\r\n\r\n"; + print "Requested simulation output was not found.\n"; + exit 0; + } + + open(my $input_file, "<", $read_filename) or do { + print "Status: 500 Internal Server Error\r\n"; + print "Content-Type: text/plain; charset=UTF-8\r\n\r\n"; + print "Requested simulation output could not be read.\n"; + exit 0; + }; + print "Content-Type: text/plain; charset=UTF-8\r\n\r\n"; + while (read($input_file, my $buffer, 65536)) { + print $buffer; + } + close($input_file); + exit 0; +} + +if ($action eq "download") { + my $download_run = $in->param("run") // ""; + my $archive_path = "$workroot/$download_run.tgz"; + if ($download_run !~ /^[A-Za-z0-9_-]+$/ || !-f $archive_path) { + print "Status: 404 Not Found\r\n"; + print "Content-Type: text/plain; charset=UTF-8\r\n\r\n"; + print "Requested results archive was not found.\n"; + exit 0; + } + + open(my $archive_file, "<", $archive_path) or do { + print "Status: 500 Internal Server Error\r\n"; + print "Content-Type: text/plain; charset=UTF-8\r\n\r\n"; + print "Requested results archive could not be read.\n"; + exit 0; + }; + binmode($archive_file); + binmode(STDOUT); + print "Content-Type: application/gzip\r\n"; + print "Content-Disposition: attachment; filename=\"$download_run.tgz\"\r\n"; + print "Content-Length: " . (-s $archive_path) . "\r\n\r\n"; + while (read($archive_file, my $buffer, 65536)) { + print $buffer; + } + close($archive_file); + exit 0; +} + +my $requested_filename = $in->param("fn") // ""; +my $rng_type = $in->param("rngType") // "pcg32"; + +# Accept only the private run path produced by TrimSPweb.js. Restricting both +# path components prevents CGI parameters from selecting arbitrary files. +if ($requested_filename !~ m{^/tmp/([A-Za-z0-9_-]+)/([A-Za-z0-9_.-]+)$}) { + print "Status: 400 Bad Request\r\n"; + print "Content-Type: application/json\r\n\r\n"; + print encode_json({ + ok => JSON::PP::false, + phase => "request", + message => "Invalid simulation filename", + }); + exit 0; +} + +my ($run_name, $file_name) = ($1, $2); +$rng_type = "pcg32" unless $rng_type =~ /^(?:ranlux|pcg32|xoshiro|xoshiro256)$/; + +my $work_path = "$workroot/$run_name"; +my $filename = "$work_path/$file_name"; +my $run_base = $file_name; +$run_base =~ s/\.inp$//; + +# Reconstruct submitted file content in numeric line order. CGI parameter +# iteration order is not guaranteed for line10, line11, and later fields. +my @line_parameters = sort { + ($a =~ /line(\d+)/)[0] <=> ($b =~ /line(\d+)/)[0] +} grep { /^line\d+$/ } $in->param; +my $content = join("\n", map { scalar($in->param($_) // "") } @line_parameters) . "\n"; + +make_path($work_path, { mode => 0755 }) unless -d $work_path; +open(my $output_file, ">", $filename) or do { + print "Status: 500 Internal Server Error\r\n"; + print "Content-Type: application/json\r\n\r\n"; + print encode_json({ + ok => JSON::PP::false, + phase => "write", + message => "Could not write the submitted simulation file", + }); + exit 0; +}; +print {$output_file} $content; +close($output_file); + +my $phase = "save"; +my $message = "Saved $file_name"; +my $exit_code; +my $archive_url; +my $log_content = ""; +my $request_logfile; +my $ok = JSON::PP::true; + +if ($filename =~ /_Seq_Results\.dat$/) { + $phase = "archive"; + unlink("$work_path/edist"); + + # Only simulation files live in the run directory. Diagnostic output is + # captured separately and is therefore excluded from the archive. + my $archive_path = "$workroot/$run_name.tgz"; + my ($log_handle, $logfile) = tempfile("trimsp-$run_name-XXXXXX", DIR => $workroot, UNLINK => 0); + close($log_handle); + $request_logfile = $logfile; + my $command = "tar -czf '$archive_path' -C '$work_path' . > '$logfile' 2>&1"; + system($command); + $exit_code = $? == -1 ? 127 : $? >> 8; + if ($exit_code == 0) { + $archive_url = "/cgi-bin/singleTrimSP.cgi?action=download&run=$run_name"; + $message = "Created results archive"; + remove_tree($work_path); + } else { + $ok = JSON::PP::false; + $message = "Could not create results archive"; + } +} elsif ($filename !~ /TrimSP\.cfg$/) { + $phase = "simulation"; + my ($log_handle, $logfile) = tempfile("trimsp-$run_name-XXXXXX", DIR => $workroot, UNLINK => 0); + close($log_handle); + $request_logfile = $logfile; + + # All user-derived command components were restricted above. Each request + # receives a private log file so concurrent users never share output. + my $command = "cd '$work_path' && timeout 300 '$trimsp_bin' " + . "'$run_base' '$rng_type' >> '$logfile' 2>&1"; + system($command); + $exit_code = $? == -1 ? 127 : $? >> 8; + $ok = JSON::PP::false if $exit_code != 0; + $message = $exit_code == 0 + ? "TRIM.SP-NL completed $run_base" + : "TRIM.SP-NL failed while running $run_base"; +} + +# Return at most the latest 64 KiB to the browser and immediately remove the +# temporary server-side copy. Diagnostic logs never enter the results archive. +if ($request_logfile && -f $request_logfile && open(my $log_handle, "<", $request_logfile)) { + my $log_size = -s $log_handle; + seek($log_handle, $log_size - 65536, 0) if $log_size > 65536; + local $/; + $log_content = <$log_handle> // ""; + close($log_handle); + unlink($request_logfile); +} + +print "Content-Type: application/json\r\n\r\n"; +print encode_json({ + ok => $ok, + phase => $phase, + message => $message, + exitCode => $exit_code, + archiveUrl => $archive_url, + log => $log_content, +}); + +exit 0;