From a34e012eee1857b8bf521f3979b7d73779513f2f Mon Sep 17 00:00:00 2001 From: John Cardinal Date: Thu, 22 Oct 2026 20:25:00 -0700 Subject: [PATCH] case 4690 - report browser downloads under the data path, not beside the app; manual updated Co-Authored-By: Claude Opus 5.5 --- devdocs/modernization/NET10-PORT.md | 5 +++-- .../ops-config-report-render-browser-path.md | 4 +++- .../ayanova/docs/ops-install-linux-server.md | 22 +------------------ .../ayanova/docs/ops-upgrade-linux-desktop.md | 6 ++--- .../ayanova/docs/ops-upgrade-linux-server.md | 10 +++++---- server/AyaNova/biz/ReportBiz.cs | 10 +++++++-- server/AyaNova/util/ServerBootConfig.cs | 9 ++++++++ 7 files changed, 32 insertions(+), 34 deletions(-) diff --git a/devdocs/modernization/NET10-PORT.md b/devdocs/modernization/NET10-PORT.md index 471db5ef..c4be0e63 100644 --- a/devdocs/modernization/NET10-PORT.md +++ b/devdocs/modernization/NET10-PORT.md @@ -117,7 +117,7 @@ in this branch or a follow-up release. UPDATE: John has decided these all go in - [x] **`DbUtil.AdminConnectionString`.** Fixed with the builder below (Sockeye 8.1.0 still has the `Replace`). Confirmed: `DbUtil.cs:99` is `_dbConnectionString.Replace(_dbName, "postgres")`, used by the boot check (`:192`), create (`:286`) and drop/recreate (`:334`). Any customer whose role name, password or host contains the database name as text gets a mangled admin connection, and the boot check authenticates as the wrong user. Fix: `new NpgsqlConnectionStringBuilder(cs) { Database = "postgres" }.ConnectionString` - [x] **Backup `pg_dump` call.** Fixed (Sockeye 8.1.0 still has the old call): new `RunProgram.RunNoShell` (no shell, `ArgumentList`, per-process environment, both streams read together, timeout kills the process tree, non-zero exit code returned as an error); backup passes `--host/--port/--username/--dbname/--format=custom/--file/--no-password` with the password in `PGPASSWORD`. The old `RunProgram.Run` now has no callers (see Deferred). Confirmed: `CoreJobBackup.cs:68` builds `postgresql://user:password@host:port/db` with the raw password, and `:94` adds `-Fc > file`; `RunProgram` runs it through `cmd.exe /C` (`RunProgram.cs:55`) or `/bin/sh -c` (`:82-89`). A password with spaces or `@ : / ? # %` breaks backups. Also, on Linux stderr is not redirected (`RunProgram.cs:86-92`), so a failed pg_dump returns empty output and is **reported as a successful backup** (`CoreJobBackup.cs:97`). Fix: password in `PGPASSWORD` in the process environment, connection parameters as separate arguments through `ProcessStartInfo.ArgumentList` (no shell), output with `-f`, stderr and exit code checked - [x] **Drop-and-recreate database.** Fixed: before the `DROP`, `DropAndRecreateDbAsync` checks `rolcreatedb OR rolsuper` for the current user; without it, boot stops with an E1000 message naming the user and the setting, and the database is untouched. Confirmed: `DbUtil.cs:342-345` drops, then creates; triggered by `AYANOVA_PERMANENTLY_ERASE_DATABASE` (`Startup.cs:567-570`). Without `CREATEDB` it drops the database and then fails to create it. Test the permission before dropping, or change the order -- [ ] **Report browser download location.** Confirmed: `new BrowserFetcher()` with the default path beside the app (`ReportBiz.cs:800`). Point it under the data directory in code. Beside the app it is read-only under a hardened systemd unit (`ProtectSystem=strict`), and a redeploy that replaces the app folder deletes it +- [x] **Report browser download location.** Fixed: downloads into `/report-browser` (derived `ServerBootConfig.REPORT_RENDER_BROWSER_DOWNLOAD_PATH`, not a new setting; empty when no data path is set, then beside the app as before) through `BrowserFetcherOptions.Path`, and launches the downloaded executable explicitly. `AYANOVA_REPORT_RENDER_BROWSER_PATH` still takes precedence. Existing installs keep an unused old download beside the app until it is removed. Confirmed: `new BrowserFetcher()` with the default path beside the app (`ReportBiz.cs:800`). Point it under the data directory in code. Beside the app it is read-only under a hardened systemd unit (`ProtectSystem=strict`), and a redeploy that replaces the app folder deletes it - [ ] **Report failures: return a specific error.** The Sockeye crash is n/a: rendering runs as a job, and `JobsBiz.ProcessJobAsync` catches every exception, logs it through NLog and marks the job Failed (`JobsBiz.cs:390-439`), so the process survives. What remains: a browser download or launch failure (`ReportBiz.cs:800`, `:838`, outside the render `try`) reaches the client only as a generic job failure. Catch it in `DoRenderJob` and return a render error that says what failed - [ ] **Data-list date filters, small fixes.** Confirmed: `>=` (`:790-793`) and `<` (`:795-798`) use `MaxSeconds` and should use `ZeroSeconds`; date `NotEqual` (`:806-814`) uses `MaxSeconds` on both sides and should exclude the whole minute (`< ZeroSeconds OR > MaxSeconds`). `DataListField` is dereferenced at `:24` and `:27` before the DEBUG-only null check at `:33`, so **release builds** throw a NullReferenceException on an unknown column, not only DEBUG builds - [ ] **Relative date tokens use the server's time zone** (`*today*`, `*yesterday*` …). Confirmed: `ClientToday = clientTimeStamp.Date` (`:399-400`) has `Kind=Unspecified`, and `ToUniversalTime()` (`:1060`, `:1065`) converts it with the server's zone. Wrong for any user in a different zone from the server. The fix builds the range from `clientTimeStamp.Offset`, with care for ranges that cross a DST change. **This changes behaviour** and affects the integration tests' `TIME_ZONE_ADJUSTMENT`. Probably better as a follow-up release @@ -142,7 +142,7 @@ suite is waiting on (case 4653, case 4654, the contract issue). - Self-hosted Linux customers (server and desktop zips are framework-dependent) - Windows LAN: `lan.iss` prompts for the ASP.NET Core Hosting Bundle. Update its text and link to .NET 10 - Windows standalone and the launcher are self-contained: no runtime needed on the customer's machine -- [ ] Customer manual (`docs/`): system requirements and upgrade instructions for self-hosted customers +- [ ] Customer manual (`docs/`): system requirements and upgrade instructions for self-hosted customers. Report browser move (section 3): manual already updated in the same commit (`ops-config-report-render-browser-path.md`, `ops-install-linux-server.md`, `ops-upgrade-linux-server.md`, `ops-upgrade-linux-desktop.md`). The pre-8.2.0 upgrade scripts on the two upgrade pages still install `aspnetcore-runtime-8.0`: update with the .NET 10 runtime instructions - [ ] Build VM: install the .NET 10 SDK; update `devdocs/build/DEV-VM-SETUP.md` and `devdocs/build/BUILD.md` - [ ] Final gate runs on the finished code - [ ] Merge, per repo: @@ -209,3 +209,4 @@ Found during the port, out of scope. One line each. | 2026-10-22 | Bogus 35.6.5, jose-jwt 5.3.0, QRCoder 1.8.1, Newtonsoft.Json 13.0.4 direct; `appsettings.json` levels; ASP0019 n/a | Builds (only AV0015/AV0016). All packages current, none vulnerable. Login JWT still `HS256` and accepted on every authenticated call; `GET auth/totp` returns a valid 183×183 QR PNG and leaves 2FA disabled. Integration suite = baseline. Bogus (evaluation seeder) compiled only: running the seeder replaces the test data | | 2026-10-22 | Backup `pg_dump` without a shell; password in `PGPASSWORD` | On-demand backups through the API into a scratch folder with a space in its name, checked with `pg_restore --list`. Old code as `postgres`: 803 entries. New code as `postgres`: 803 entries. Failing stand-in `pg_dump` (exit code 2): reported as `BACKUP ERROR`, no file, no password in the log. Temporary superuser role `"AyaNova"` (named like the database) with password `p @:/?#% x`: old code failed (`too many command-line arguments (first is "@:/?#%")`), left a 0-byte backup file and echoed part of the password into the log; new code 803 entries, no errors. Role dropped afterwards. Integration suite = baseline. Linux not tested here: the stderr gap there is closed by the same code (no shell, exit code checked) | | 2026-10-22 | Drop-and-recreate: CREATEDB check before the drop | Throwaway non-superuser role `ayatest_nocreatedb` owning throwaway database `ayatest_erase`, server data path in scratch (the erase also clears the attachments folder). Without CREATEDB: boot stops with the new E1000 message, database still there. Old sequence by hand as the same role: `DROP DATABASE` succeeds, `CREATE DATABASE` fails "permission denied", database gone. With CREATEDB: erased and recreated (oid 145595 → 145596), schema 20 built, boot complete. Database and role dropped afterwards. Integration suite not rerun: the change is only reached with the erase setting | +| 2026-10-22 | Report browser downloaded under the data path | Browsers in `bin` moved aside; server with an empty scratch data folder. 22/22 reports render; Chrome and ChromeHeadlessShell 155 downloaded into `/report-browser`, launched from there (logged), nothing written to `bin`. Second run: first render 1.6 s (was 25 s with the download), no new files: download reused | diff --git a/docs/8.0/ayanova/docs/ops-config-report-render-browser-path.md b/docs/8.0/ayanova/docs/ops-config-report-render-browser-path.md index 56761b4a..0f561a84 100644 --- a/docs/8.0/ayanova/docs/ops-config-report-render-browser-path.md +++ b/docs/8.0/ayanova/docs/ops-config-report-render-browser-path.md @@ -2,7 +2,9 @@ This optional setting sets the _full_ path to the Chrome web browser executable that is used by AyaNova to render reports. -If not set, AyaNova will attempt to automatically download a copy of Chrome to a `Chrome` and `ChromeHeadlessShell` folders under the AyaNova executable folder on first report rendering. +If not set, AyaNova will attempt to automatically download a copy of Chrome to `Chrome` and `ChromeHeadlessShell` folders inside a `report-browser` folder under the [data path](ops-config-data-path.md) on first report rendering. The data folder must allow programs to run from it (on Linux it must not be on a `noexec` mount). + +Versions before 8.3.0 downloaded Chrome to the AyaNova program folder instead. Those `Chrome` and `ChromeHeadlessShell` folders in the program folder are no longer used and can be deleted. This setting is typically not required to be set. diff --git a/docs/8.0/ayanova/docs/ops-install-linux-server.md b/docs/8.0/ayanova/docs/ops-install-linux-server.md index ea56a9b2..bbf085b4 100644 --- a/docs/8.0/ayanova/docs/ops-install-linux-server.md +++ b/docs/8.0/ayanova/docs/ops-install-linux-server.md @@ -154,15 +154,7 @@ Create the data directory. AyaNova will automatically create subfolders under th sudo mkdir /var/ayanova/data ``` -Create the Chrome binary directories: - -```bash -sudo mkdir /var/ayanova/Chrome -``` - -```bash -sudo mkdir /var/ayanova/ChromeHeadlessShell -``` +The Chrome report rendering engine is downloaded automatically into a `report-browser` folder under this data directory the first time a report is rendered, so the data directory must not be on a `noexec` mount. ### 7\. Download and unzip the AyaNova binaries archive @@ -234,18 +226,6 @@ Set permissions to read / write for the AyaNova data directory: sudo chmod -R g+rw /var/ayanova/data ``` -Set permissions to read / write / execute for the .Chrome directory: - -```bash -chmod -R g+rwx /var/ayanova/Chrome -``` - -Set permissions to read / write / execute for the .ChromeHeadlessShell directory: - -```bash -chmod -R g+rwx /var/ayanova/ChromeHeadlessShell -``` - Set permissions for the AyaNova folder ```bash diff --git a/docs/8.0/ayanova/docs/ops-upgrade-linux-desktop.md b/docs/8.0/ayanova/docs/ops-upgrade-linux-desktop.md index 6206606f..e982f3c5 100644 --- a/docs/8.0/ayanova/docs/ops-upgrade-linux-desktop.md +++ b/docs/8.0/ayanova/docs/ops-upgrade-linux-desktop.md @@ -31,11 +31,7 @@ From a terminal window run the following commands: ```bash sudo apt-get update && apt-get upgrade && \ sudo apt-get install -y aspnetcore-runtime-8.0 && \ -sudo mkdir /var/ayanova/Chrome && \ -sudo mkdir /var/ayanova/ChromeHeadlessShell && \ sudo chown -vR :www-data /var/ayanova && \ -sudo chmod -R g+rwx /var/ayanova/Chrome && \ -sudo chmod -R g+rwx /var/ayanova/ChromeHeadlessShell && \ sudo chmod g+rw /var/ayanova/ ``` @@ -64,6 +60,8 @@ sudo ./AyaNova If there is an issue or error break apart the commands and execute each one individually to find the error. +From 8.3.0 the Chrome report rendering engine is downloaded into a `report-browser` folder under the data directory instead of the program folder. After upgrading from an earlier version any old `Chrome` and `ChromeHeadlessShell` folders in the AyaNova program folder are no longer used and can be removed. + AyaNova should restart on it's own and perform any database schema updates if required automatically. Open the login form in your browser and after a short delay you should see the new version number at the bottom of the login form. diff --git a/docs/8.0/ayanova/docs/ops-upgrade-linux-server.md b/docs/8.0/ayanova/docs/ops-upgrade-linux-server.md index cbfe6851..308a557e 100644 --- a/docs/8.0/ayanova/docs/ops-upgrade-linux-server.md +++ b/docs/8.0/ayanova/docs/ops-upgrade-linux-server.md @@ -25,11 +25,7 @@ From a terminal window run the following commands: ```bash sudo apt-get update && apt-get upgrade && \ sudo apt-get install -y aspnetcore-runtime-8.0 && \ -sudo mkdir /var/ayanova/Chrome && \ -sudo mkdir /var/ayanova/ChromeHeadlessShell && \ sudo chown -vR :www-data /var/ayanova && \ -sudo chmod -R g+rwx /var/ayanova/Chrome && \ -sudo chmod -R g+rwx /var/ayanova/ChromeHeadlessShell && \ sudo chmod g+rw /var/ayanova/ ``` @@ -58,6 +54,12 @@ sudo systemctl start ayanova.service If there is an issue or error break apart the commands and execute each one individually to find the error. +From 8.3.0 the Chrome report rendering engine is downloaded into a `report-browser` folder under the data directory instead of the program folder. After upgrading from an earlier version the old folders are no longer used and can be removed: + +```bash +sudo rm -r /var/ayanova/Chrome /var/ayanova/ChromeHeadlessShell +``` + AyaNova should restart on it's own and perform any database schema updates if required automatically. Open the login form in your browser and after a short delay you should see the new version number at the bottom of the login form. diff --git a/server/AyaNova/biz/ReportBiz.cs b/server/AyaNova/biz/ReportBiz.cs index 1280616f..3ac669c5 100644 --- a/server/AyaNova/biz/ReportBiz.cs +++ b/server/AyaNova/biz/ReportBiz.cs @@ -795,8 +795,14 @@ What could be the issue here? //case 4586 //puppeteer has gone chrome dev by default and so does puppeteersharp which tests ok so I'm going to allow the default - //this means the files go into binary folder under Chrome and ChromeHeadlessShell - await new BrowserFetcher().DownloadAsync(); + //this means the files go into Chrome and ChromeHeadlessShell folders, under the data path (not beside the app, see ServerBootConfig) + var FetcherOptions = new BrowserFetcherOptions(); + if (!string.IsNullOrWhiteSpace(ServerBootConfig.REPORT_RENDER_BROWSER_DOWNLOAD_PATH)) + FetcherOptions.Path = ServerBootConfig.REPORT_RENDER_BROWSER_DOWNLOAD_PATH; + var InstalledBrowser = await new BrowserFetcher(FetcherOptions).DownloadAsync(); + //launch exactly what was downloaded, otherwise Puppeteer looks in its default folder beside the app + lo.ExecutablePath = InstalledBrowser.GetExecutablePath(); + log.LogDebug($"Report browser: {lo.ExecutablePath}"); } //Set Chromium args diff --git a/server/AyaNova/util/ServerBootConfig.cs b/server/AyaNova/util/ServerBootConfig.cs index dacd7737..cb1a28f5 100644 --- a/server/AyaNova/util/ServerBootConfig.cs +++ b/server/AyaNova/util/ServerBootConfig.cs @@ -87,6 +87,11 @@ namespace AyaNova.Util //REPORT RENDERING BROWSER PARAMS internal static string AYANOVA_REPORT_RENDER_BROWSER_PARAMS { get; set; } + //REPORT RENDERING BROWSER DOWNLOAD FOLDER (not a setting, derived from AYANOVA_DATA_PATH) + //the auto-downloaded browser goes here instead of beside the app: the app folder can be read-only (hardened systemd unit) + //and is replaced on redeploy, which would delete the download. Empty when there is no data path: download beside the app as before + internal static string REPORT_RENDER_BROWSER_DOWNLOAD_PATH { get; set; } + //REPORT RENDERING API URL OVERRIDE internal static string AYANOVA_REPORT_RENDER_API_URL_OVERRIDE { get; set; } @@ -261,6 +266,10 @@ namespace AyaNova.Util (string.IsNullOrWhiteSpace(TempFilesPath)) ? Path.Combine(DataFolderPath, "temp") : TempFilesPath; + REPORT_RENDER_BROWSER_DOWNLOAD_PATH = + (string.IsNullOrWhiteSpace(DataFolderPath)) + ? string.Empty + : Path.Combine(DataFolderPath, "report-browser"); #endregion server BASICS }