From f3e8026501fa07e4840012916311083a15d9ce2e Mon Sep 17 00:00:00 2001 From: John Cardinal Date: Thu, 22 Oct 2026 18:12:49 -0700 Subject: [PATCH] case 4690 - PuppeteerSharp 25.15.0; launch arguments as separate elements; --lang keeps the server locale as browser default Co-Authored-By: Claude Opus 5.5 --- devdocs/modernization/NET10-PORT.md | 8 +++++--- server/AyaNova/AyaNova.csproj | 2 +- server/AyaNova/biz/ReportBiz.cs | 30 ++++++++++++++++++++++------- 3 files changed, 29 insertions(+), 11 deletions(-) diff --git a/devdocs/modernization/NET10-PORT.md b/devdocs/modernization/NET10-PORT.md index 93ce8eeb..7560fd49 100644 --- a/devdocs/modernization/NET10-PORT.md +++ b/devdocs/modernization/NET10-PORT.md @@ -88,11 +88,11 @@ them. Items touching those three areas carry more weight here than they did in S - [ ] **Newtonsoft.Json.** `Mvc.NewtonsoftJson` 10.x sets a 13.0.3 floor (NU1605). Already resolves 13.0.3 transitively; add it as a direct reference - [ ] **JwtBearer 10.x.** The IdentityModel part is n/a: it resolves 7.1.2, not the vulnerable 6.10.0 - [x] **SMTP.** n/a: `util/Mailer.cs` already uses MailKit's `SmtpClient` (`:1`, `:84`); no `System.Net.Mail` -- [ ] **PuppeteerSharp 17 → 25.x.** Reporting is AyaNova's main Puppeteer surface (`biz/ReportBiz.cs`) +- [x] **PuppeteerSharp 17 → 25.x.** Reporting is AyaNova's main Puppeteer surface (`biz/ReportBiz.cs`). Done: 25.15.0 (Sockeye: 25.12.0); compiles with no API changes; launch arguments fixed (below). Downloads Chrome for Testing 155 (was 124). The download location and failure handling are separate section 3 items - [x] `BrowserFetcher.DefaultChromiumRevision`: n/a, already `DownloadAsync()` with no argument (`ReportBiz.cs:800`, case 4586) - [x] `IJSHandle.ExecutionContext`: n/a, the only use is commented out (`ReportBiz.cs:858-860`). Evaluate results are `JsonElement?`: check any remaining `Evaluate*` call sites - The default download is Chrome for Testing, into `Chrome/` and `ChromeHeadlessShell/` beside the app. AyaNova 8.2.0 already moved from Chromium to Chrome to follow Puppeteer's default, so the browser choice is settled - - [ ] Launch arguments must be separate array elements. **Both paths are wrong today:** the defaults are one element `"--headless --no-sandbox "` (`ReportBiz.cs:812`, `:829`), and `AYANOVA_REPORT_RENDER_BROWSER_PARAMS` is passed whole as one element (`:819-822`) + - [x] Launch arguments must be separate array elements. Fixed: defaults are `new[] { "--no-sandbox" }` (`--headless` dropped, `LaunchOptions.Headless` already adds it, as in Sockeye); the customer setting is split on whitespace. A customer value containing a quoted argument with spaces would now be split; none is known. **Browser default locale:** the old `--headless` argument ran Chrome's old headless mode, which takes its default language from the OS (en-CA → Chrome's en-GB, day-first); current Chrome headless always defaults to en-US. Report scripts that format without a locale (stock sample report 80 calls `toLocaleDateString()` bare) switched from 22/10/2026 to 10/22/2026. Fixed by adding `--lang=` unless the customer setting already has a `--lang`; skipped when the server culture is invariant. Stock helpers in `ay-report.js` pass the client's language explicitly and were never affected. **Both paths were wrong:** the defaults are one element `"--headless --no-sandbox "` (`ReportBiz.cs:812`, `:829`), and `AYANOVA_REPORT_RENDER_BROWSER_PARAMS` is passed whole as one element (`:819-822`) - AppArmor restricts unprivileged user namespaces on Ubuntu 24.04+, which affects the Chrome sandbox - [ ] Remaining packages to current releases. MailKit 4.5.0 carried a moderate advisory (NU1902); on `net10.0` the audit also reported high-severity NU1903 for `MimeKit` 4.5.0 and transitive `System.Security.Cryptography.Pkcs` 8.0.0. **Mail done:** MailKit/MimeKit 4.18.1, BouncyCastle 2.7.0, and `System.Security.Cryptography.Pkcs` 10.0.13 pinned directly (MimeKit only requires 10.0.0, which has advisories). `dotnet list package --vulnerable --include-transitive`: none. After the upgrade, check what still pulls in `System.Text.Json` 5.0.2 - [ ] Warnings: CA2022 → `ReadExactly` at `FileUtil.cs:875` (**licence build-date path**: covered by the licence gate); ASP0019 → header indexer instead of `Headers.Append` at `Startup.cs:514` @@ -180,7 +180,7 @@ Found during the port, out of scope. One line each. - `License.DoFetchAsync` (`License.cs:629-661`) rethrows at `x == 3`, so the fifth server (Sockeye) is never tried, and it can return `null` to a caller that dereferences it - Stray `raven-client\ayanova\cypress.json`: the Cypress suite lives in `raven-test\e2e` -- **Race, affects production after every restart:** `FormFieldReference.FormFieldReferenceList` (`biz/FormFieldReference.cs:57-59`) builds a static dictionary lazily with no lock, assigning it before filling it. A concurrent first request sees it half-built and throws `FormFieldReferenceList: is not valid`. Seen once (`VendorCrud.CRUD`) on the first .NET 10 suite run after boot. Fix: build the dictionary fully before publishing it (`Lazy` or a static initialiser). Pre-existing, not a port change. John's decision: its own case, fixed with the other cases going into this release, not under the port +- **Race, affects production after every restart:** `FormFieldReference.FormFieldReferenceList` (`biz/FormFieldReference.cs:57-59`) builds a static dictionary lazily with no lock, assigning it before filling it. A concurrent first request sees it half-built and throws `FormFieldReferenceList: is not valid`. Seen twice on .NET 10, each time on the first form-field use after boot: `VendorCrud.CRUD`, then `FormCustom.FormCustomUpdate`. Fix: build the dictionary fully before publishing it (`Lazy` or a static initialiser). Pre-existing, not a port change. John's decision: its own case, fixed with the other cases going into this release, not under the port - `raven-launcher` `Program.cs:98` calls `PGProcess.StandardInput.Close()` but standard input was never redirected, so it throws `InvalidOperationException` at shutdown: the "Postgres shut down" step never runs and the launcher exits with an error when the server stops. Postgres was started detached by `pg_ctl start`, so it probably stays running. Pre-existing; candidate for its own case - Sockeye 8.1.0 has the NLog 6 archive naming problem fixed here for AyaNova: archives are `log-sockeye_NN.txt`, old `-0` to `-8` archives are never deleted, and its log viewer still sorts by name (it also filters on `ayanova.txt`, not `sockeye.txt`, so the current file is listed twice). Same fix applies: `ArchiveSuffixFormat = "-{0}"` and the date sort @@ -199,3 +199,5 @@ Found during the port, out of scope. One line each. | 2026-10-22 | MailKit/MimeKit 4.5.0 → 4.18.1, BouncyCastle 2.3.1 → 2.7.0, Pkcs 10.0.13 pinned | Builds (only CA2022 left); no vulnerable packages. Licence gate 730/730, per-row outcomes identical to before. Mail end to end: `test-smtp-settings` sent through `Mailer` (connect, AUTH, send) to a local SMTP sink: well-formed `text/plain; charset=utf-8` message received. Integration suite = baseline. PDF-attachment path not exercised (report emailing) | | 2026-10-22 | Swashbuckle 6.5.0 → 10.3.1, OpenAPI.NET 1.6 → 2.7.5 | Builds (only CA2022). `/swagger/v8/swagger.json` compared with the pre-change document: same 315 paths, 462 operations (ids, parameters, request bodies, response codes) and 137 schemas; same global Bearer requirement and scheme. Only change: `openapi` 3.0.1 → 3.0.4. `/api-docs` UI serves. Integration suite = baseline | | 2026-10-22 | NLog 5.3 → 6.2.2; archive suffix `-{0}`; log viewer sorted by file date | Builds (only CA2022). Server at Trace level, logging to a scratch folder seeded with NLog 5 archives `-0` to `-8`, through suite runs: `log-ayanova.txt` current, 9 archives `log-ayanova-23` to `-31`, old files removed; `GET log-file` lists the current file, then `-31` down to `-23`. Integration suite = baseline. In one of six Trace-level runs `LogFiles.MostRecentLogShouldFetch` most likely failed: a just-rolled current file can hold only TRACE/DEBUG lines (not reproduced; not at Info level) | +| 2026-10-22 | PuppeteerSharp 17 → 25.15.0; launch arguments as separate elements | Builds (only CA2022). Rendered all 24 work order and customer reports through the API (`render-job`, poll, download) for records 1 and 2: on PuppeteerSharp 17 (baseline), on 25 with default arguments, and on 25 with `AYANOVA_REPORT_RENDER_BROWSER_PARAMS="--no-sandbox --disable-gpu"`. 24/24 ok each time, page counts identical; PDFs 5–20% smaller with Chrome 155. Integration suite = baseline. PDFs kept in the scratchpad for John's visual check | +| 2026-10-22 | John's visual check of the PuppeteerSharp 25 PDFs; `--lang` fix | Identical apart from report 80: date month/day flipped (default locale, see section 1). Probe of Chrome 124/155 with each argument set confirmed the cause. With `--lang=`: John confirmed report 80 shows 22/10/2026 again; 22/22 render, page counts = PuppeteerSharp 17, launch arguments logged as `--no-sandbox --lang=en-CA`. Integration suite: 95 passed, 3 failed: the two known plus `FormCustom.FormCustomUpdate`, which the server log shows was the `FormFieldReference` start-up race again (`Vendor is not valid` at the first form-field use after boot; see Deferred), not this change. **Never render reports 81 (sends real email via the server's SMTP settings) or 86 (writes to the work order) in tests**; 81 ran three times on 2026-10-22 and caused bounces | diff --git a/server/AyaNova/AyaNova.csproj b/server/AyaNova/AyaNova.csproj index 4a875f07..bd685c6b 100644 --- a/server/AyaNova/AyaNova.csproj +++ b/server/AyaNova/AyaNova.csproj @@ -51,7 +51,7 @@ - + diff --git a/server/AyaNova/biz/ReportBiz.cs b/server/AyaNova/biz/ReportBiz.cs index 6d2a09c4..ef1301c4 100644 --- a/server/AyaNova/biz/ReportBiz.cs +++ b/server/AyaNova/biz/ReportBiz.cs @@ -809,25 +809,41 @@ What could be the issue here? //Keep for debugging headfully // var DefaultArgs = "--no-sandbox"; - var DefaultArgs = "--headless --no-sandbox "; // --ignore-certificate-errors (potential proposal, not implemented yet for case 4632 saving here in case want to try it) + //PuppeteerSharp 25: each argument must be its own array element, a single "--a --b" string is passed to the browser as one unknown switch + //--headless is not needed here, LaunchOptions.Headless already adds it + var DefaultArgs = new[] { "--no-sandbox" }; // --ignore-certificate-errors (potential proposal, not implemented yet for case 4632 saving here in case want to try it) if (!string.IsNullOrWhiteSpace(ServerBootConfig.AYANOVA_REPORT_RENDER_BROWSER_PARAMS)) { log.LogDebug( $"AYANOVA_REPORT_RENDER_BROWSER_PARAMS will be used: {ServerBootConfig.AYANOVA_REPORT_RENDER_BROWSER_PARAMS}" ); - lo.Args = new string[] - { - $"{ServerBootConfig.AYANOVA_REPORT_RENDER_BROWSER_PARAMS}", - }; // AYANOVA_REPORT_RENDER_BROWSER_PARAMS + //customer setting is one space separated string, split it into separate arguments + lo.Args = ServerBootConfig.AYANOVA_REPORT_RENDER_BROWSER_PARAMS.Split( + (char[])null, + StringSplitOptions.RemoveEmptyEntries + ); // AYANOVA_REPORT_RENDER_BROWSER_PARAMS } else { log.LogDebug( - $"AYANOVA_REPORT_RENDER_BROWSER_PARAMS not set, using defaults '{DefaultArgs}'" + $"AYANOVA_REPORT_RENDER_BROWSER_PARAMS not set, using defaults '{string.Join(" ", DefaultArgs)}'" ); - lo.Args = new string[] { DefaultArgs }; + lo.Args = DefaultArgs; } + + //Browser default locale follows the server OS, as it did before PuppeteerSharp 25 (old headless mode took it from the OS, + //current Chrome headless always defaults to en-US). Matters to report scripts that format without a locale, e.g. toLocaleDateString() + //A --lang in AYANOVA_REPORT_RENDER_BROWSER_PARAMS wins + var ServerLanguage = System.Globalization.CultureInfo.CurrentCulture.Name; + if ( + !string.IsNullOrWhiteSpace(ServerLanguage) + && !lo.Args.Any(z => z.StartsWith("--lang", StringComparison.OrdinalIgnoreCase)) + ) + { + lo.Args = lo.Args.Append($"--lang={ServerLanguage}").ToArray(); + } + log.LogDebug($"Report browser launch arguments: '{string.Join(" ", lo.Args)}'"); System.Text.StringBuilder PageLog = new System.Text.StringBuilder(); if (!ReportRenderManager.KeepGoing(job.GId))