Import sibling SharedOutputFiles folder during log import - #558
Import sibling SharedOutputFiles folder during log import#558JosephPilov-MSFT (PiJoCoder) wants to merge 21 commits into
Conversation
Adds support for SQL LogScout "All Instances" captures where non-instance-specific (host/OS) logs live in a sibling SharedOutputFiles folder next to the pointed instance folder. When that sibling exists, SQL Nexus now searches both the primary folder and the sibling; when it does not exist, behavior is unchanged. Feature: - Add SharedOutputFolder helper (single source of truth) that resolves the ordered search paths and validates the sibling is a real direct sibling named SharedOutputFiles (rejects traversal, fails closed). - fmImport: AddFiles now enumerates both folders; per-file rows record their real target path (m_RowTargetPaths) and get a cosmetic "(SharedOutput)" label suffix; import loop resolves the importer target from the recorded path instead of concatenating the primary path with the display label. Mask-based importers (e.g. Perfmon BLG) get a second row pointing at the sibling. - RawFileImporter: enumerate raw files across primary + sibling. - Existing BlockFile / instances.Block logic reused as-is (no sentinel-name filtering), so a future rename of the shared marker is unaffected. Test enablement (option 2 - unsign test project): - SqlNexus.UnitTests: SignAssembly=false so the test assembly can load the unsigned sqlnexus.exe host (unblocks all sqlnexus tests in the CLI/CI runner). Product DLLs remain strong-named. - ErrorLogImporter: remove key-qualified InternalsVisibleTo (a signed assembly cannot grant friend access to an unsigned friend) and widen the 6 test-exercised members from internal to public. Bug fix (surfaced once tests could run): - Program.IsDbNameValid: fail closed on null/empty/whitespace (Regex.IsMatch throws on null) instead of throwing. Tests: add SharedOutputFolderTests; full suite 170/170 passing.
- Fix path normalization in SharedOutputFolder to preserve rooted drive paths (e.g. "C:\" is no longer reduced to "C:"). - Normalize sibling parent comparison logic to use consistent path normalization across importers - Add fallback in fmImport.AddFiles() to use the primary import path when shared-path resolution returns no entries. - Add regression test: SharedOutputFolderTests.GetImportSearchPaths_DriveRootInput_PreservesRootedPath
There was a problem hiding this comment.
PR #558 — Change Checklist
PR: Import sibling SharedOutputFiles folder during log import · Issue: #556 · Commit reviewed: ed01cf7
Nice work — the design is right and the sibling-absent case is a true no-op. Below is everything I'd like changed, shortest-path first.
Must fix before merge
-
1. Delete the dead sibling guard —
sqlnexus/SharedOutputFolder.cs
isDirectSiblingcan never befalse(parentis already normalized andSharedFolderNamehas no separators), and the comment claiming it "rejects traversal or symlink-style tricks" is wrong —Path.GetFullPathcollapses..first, andDirectory.Existsfollows junctions. Remove the block and the comment, or add a real reparse-point check. -
2. Fix LinuxPerfImporter reading the wrong folder —
sqlnexus/fmImport.cs
ConfigValues.WorkingDirectoryis still set tosrcPathonly, andLinuxPerfImporteruses that instead of the path passed toInitialize. A*.perffile found inSharedOutputFilesgets a row that reports success while importing nothing. -
3. Log the shared folder —
sqlnexus/fmImport.cs
Nothing insqlnexus.logrecords that a second folder was searched; provenance exists only as a GUI suffix. Add a line naming the resolved sibling plus per-folder file counts. -
4. Fix the duplicated "files blocked" log line —
sqlnexus/fmImport.cs
"Number of files blocked for import..."now fires once per directory per mask instead of once per mask, so the log shows two competing numbers. -
5. Reconsider the signing change —
SqlNexus.UnitTests.csproj,ErrorLogImporter
The csproj comment says a strong-named test assembly can't load an unsigned reference — that's CS8002, a warning, already in<NoWarn>. The real blocker was thatsqlnexus'sInternalsVisibleTohas noPublicKey. RestoreSignAssembly=trueand add[assembly: InternalsVisibleTo("SqlNexus.UnitTests, PublicKey=0024...")]tosqlnexus/Properties/AssemblyInfo.cs— then ErrorLogImporter's 6 members go back tointernalinstead of becoming permanent public plugin API.
Please confirm intent (may be by design)
-
6. Sibling is scanned with every importer mask —
*.trc,*.xel,*.blg,*.out,*.txt,*.perf. #556 only asks for host/OS files. For mask-based importers this triggers a secondInitialize+DoImport; for ReadTrace that can re-run table setup over the first run's results. -
7. Duplicate file names aren't deduped — if the same name exists in both folders (e.g.
SQLDIAG*.OUT), both rows import into the same tables. Silent duplicate rows. Warn or skip. -
8. Two call sites weren't extended —
CustomXELImporter.ImportCustomXELFiles(...)and the XEL/TRC conflict pre-check at the top ofEnumFiles()still see only the primary folder.
Code quality
-
9. Log the swallowed exceptions — three
catch (Exception)blocks inSharedOutputFolder.csreturn silently. Usecatch (Exception ex)+Util.Logger.LogMessage(..., MessageOptions.Silent)per the repo's exception-handling rules. -
10.
ResolveSharedSiblingcan throw — it'spublic, its parameter is namednormalizedPrimary, but the twoNormalizePathcalls inside theisDirectSiblingexpression sit outside anytry. Make itprivateor guard them. -
11. Fix three misleading comments — the traversal/symlink claim (#1), the "no injection surface" note sitting beside dead validation code, and the CS8002 justification (#5).
-
12. Small cleanups —
AddFileRowis now a one-line pass-through toAddFileRowReturningLabel;if (includedFiles.Length > 0)is unreachable-false. Also, deletingsqlnexus/fmnexus.cs.2is good hygiene but is unrelated scope creep in a feature PR.
Accessibility / UX
-
13. Set
AccessibleNameon the new row controls —AddFileRowReturningLabelsets none on the progress bar or status label. Two rows can now differ only by a suffix, so progress bars are ambiguous to a screen reader.pb.AccessibleName = labelText;andlab2.AccessibleName = labelText + " status"; -
14. Rename the label suffix —
" (SharedOutput)"→" (from SharedOutputFiles)", derived fromSharedOutputFolder.SharedFolderNamerather than a separate literal. Reads better aloud and matches the real folder name. -
15. Add a form-level affordance — the path combo box still shows only the instance folder, so a second scanned folder is only inferable from per-row suffixes. A status label ("Also scanning
..\SharedOutputFiles") makes it discoverable.
Tests
-
16. Cover the risky half of the change.
SharedOutputFolderTestsis solid, but the part that changed behavior for every import row —m_RowTargetPathsand the display-suffix composition — has zero coverage, as doAddFilesFromDirectoryandRawFileImporter. Extracting two pure helpers (basePath+ name/mask → target path; name +isShared→ display text) makes them testable without WinForms. -
17. Add missing edge cases — UNC paths (
\\server\share\output\Instance1), quoted input (GetImportSearchPathsstrips quotes internally but that's untested), and case-insensitive folder name.
Docs
-
18. Update
TestingInfrastructure/README.md— it's now factually wrong: it lists 2 project references (there are 4) and tells contributors to useInternalsVisibleTo("SqlNexus.UnitTests"), which can no longer work for any strong-named product assembly while the test project is unsigned. -
19. Document the user-visible behavior change — Nexus now reads files from outside the folder the user selected. Worth a note in the Getting Started wiki /
Help/*.htmso users aren't surprised by rows for files they didn't point at.
Verified clean — no action needed
- No compile errors on any changed file.
- No security regressions: folder name is a compile-time constant, nothing is concatenated into SQL,
RawFileImporterstill usesSqlParameter+IsSafeSqlIdentifier. - GUI rows are consistent with existing rows (same label/progress-bar/status triple, same
Tag, same progress updates). - The
IsDbNameValidnull/empty fix is already covered by existingnull/""/" "rows inDatabaseCommandTextTests.cs.
|
Added new logging: |
…locked count ( #556 ) Addresses PR review feedback on the SharedOutputFiles sibling-folder import feature. - SharedOutputFolder.cs: Remove the dead direct-sibling guard in ResolveSharedSibling. Because the parent path is already normalized (Path.GetFullPath collapses "..") and SharedFolderName contains no separators, isDirectSibling could never be false. Also correct the misleading class comment that claimed the guard rejected traversal/symlink tricks (it did not: GetFullPath collapses ".." and Directory.Exists follows junctions). - fmImport.cs: Point ConfigValues.WorkingDirectory at the folder each Linux perf row was actually discovered in before the importer runs. Previously it was fixed to the primary srcPath, so a *.perf file found in SharedOutputFiles reported success while importing nothing. - fmImport.cs: Log shared-folder provenance to sqlnexus.log. Records whether a sibling SharedOutputFiles folder was searched, naming the resolved path and per-folder file counts (previously provenance existed only as a GUI label suffix). Adds a CountFilesInFolder helper. - fmImport.cs: Fix the duplicated "Number of files blocked for import" log line. The blocked count now accumulates across searched folders (passed by ref) and is logged once per mask instead of once per directory per mask. - SharedOutputFolderTests.cs: Add a regression test documenting that a path containing a ".." segment normalizes and still resolves the sibling shared folder.
…mportSharedOutputDir_556_pijocoder_090326
Strong-name the host and MCP executables with the shared SqlNexus.snk so the
unit test project can be strong-named too. This lets signed product assemblies
grant InternalsVisibleTo to the tests without widening members to public API.
Background: ErrorLogImporter is signed, and a signed assembly can only grant
InternalsVisibleTo to a strong-named friend. Because the test project referenced
the unsigned sqlnexus.exe, it could not be strong-named (a strong-named assembly
throws FileLoadException when loading an unsigned reference). The prior workaround
widened 6 ErrorLogImporter members to public. Signing both executables removes
that constraint; plugin loading is unaffected because importers load by path
(Assembly.LoadFile), not by strong-name identity.
Changes:
- sqlnexus.csproj: SignAssembly=true (key already referenced as SqlNexus.snk).
- SqlNexus.McpServer.csproj: SignAssembly=true + AssemblyOriginatorKeyFile.
- SqlNexus.UnitTests.csproj: SignAssembly=true + shared key; comment corrected.
- sqlnexus, ErrorLogImporter, SqlNexus.McpServer AssemblyInfo: upgrade
InternalsVisibleTo("SqlNexus.UnitTests") grants to the keyed PublicKey form.
- ErrorLogImporter.cs: revert IsHeadAndTailMarker, StripLeadingByteOrderMark,
AdvancePosition, ProcessLogEntries, and the two INCOMPLETE_* constants from
public back to internal.
Notes:
- All assemblies in the reference graph share public key token cdc5595d1bc5db30.
- Reuses the existing (already public) SqlNexus.snk; no new exposure. Strong names
are an identity mechanism, not a security control.
- Compatible with subsequent Authenticode signing (apply signtool post-build).
- Verified: full solution builds; 267/267 unit tests pass; sn -vf valid.
…a gaps Add safeguards to the SharedOutputFiles sibling-folder import so that files present in both the primary instance folder and the shared folder are not imported twice, and files that live only in the shared folder are surfaced. Guards (all log with a coherent "Shared folder: skipping duplicate ..." prefix): - Per-file importers (e.g. Rowset .OUT/.TXT): a sibling file whose name matches one already selected from the primary folder is skipped (primary wins). Size is compared only to choose the message: same size = concise skip; different or unreadable size = louder WARNING naming both copies so the user can import the sibling copy manually if the captures are genuinely different. - Mask-based/aggregating importers (e.g. BLG Blaster, ReadTrace): if the primary folder already provided files for a mask, the sibling mask scan is skipped so a second Initialize+DoImport does not re-run table setup over already-imported data. The sibling remains a fallback when the primary matched nothing (no lost import opportunity). - Custom XEL (SQLDiag / AlwaysOn_health / system_health): these are handled by a separate call site that reads the primary folder only. Warn about Custom XEL files that exist ONLY in the shared folder (the real data gap); files present in both folders are ignored (primary copy is imported, so no gap). Never auto- imports the sibling - the Custom XEL load uses SELECT * INTO with optional DROP TABLE and would overwrite already-imported tables. - The trace/xevent conflict pre-check in EnumFiles() now inspects both folders so a cross-folder .trc + .xel conflict is detected. Pure, unit-tested helpers added to SharedOutputFolder: - FilterDuplicateSiblingFiles(): size-aware duplicate classification. - GetSiblingOnlyFiles(): shared-folder-only file detection. Tests: 23 SharedOutputFolder tests (full suite 281 passing), including a regression for a *.TXT copied into both folders and a system_health.xel present in both folders (no false gap warning).
…I/accessibility) Refine the SharedOutputFiles sibling-folder import feature per PR review feedback. - SharedOutputFolder.cs (#9): the three catch (Exception) blocks now log via a null-safe LogSilent helper (Util.Logger + MessageOptions.Silent, falling back to Debug.WriteLine when no logger is set) instead of returning silently. - SharedOutputFolder.cs (#11): correct the class-level comment - the primary path IS user input, but the folder NAME is a fixed constant and the resolved path is only used to enumerate files (never to build SQL/shell), so it is not itself an injection surface. The stale traversal/symlink claim was already removed earlier. - SharedOutputFolder.cs (#10): ResolveSharedSibling is confirmed throw-safe - both NormalizePath calls are inside guarded try blocks. - SqlNexus.UnitTests.csproj (#11): fix the CS8002 justification comment - 8002 is suppressed for non-strong-named test-only NuGet assemblies, not because all references are signed. - fmImport.cs (#12): remove the unreachable-false 'if (includedFiles.Length > 0)' branch (the empty case already returned earlier). - fmImport.cs (#13): set AccessibleName on the dynamically created progress bar (labelText) and status label (labelText + " status") so screen readers can tell rows apart when they differ only by the shared-folder suffix. - fmImport.cs (#14): rename the row suffix " (SharedOutput)" -> " (from SharedOutputFiles)", derived from SharedOutputFolder.SharedFolderName. - fmImport.cs / fmImport.Designer.cs (#15): add a form-level affordance - a muted info-band label (laSharedFolder) under the path box reading "Also scanning: <abbrev path>" with the full path in a tooltip and AccessibleName. The compact form grows by one row so the label sits below the Import/Close buttons (no overlap), and the info colors are re-asserted at show time so ThemeManager (Aquatic/Desert) does not wipe them. Path is abbreviated to <root>\...\<last-two-segments> for long folders. No behavior change to import logic; full unit test suite (281) passes.
a9b7baf to
b202f59
Compare
…iew items 16-19) Cover the behavior-changing part of the shared-folder import and correct the docs that the feature made inaccurate. - SharedOutputFolder.cs (#16): extract two pure, WinForms-free helpers so the row composition that changed for every import is unit-testable: * ComposeRowTargetPath(basePath, fileNameOrMask) - the m_RowTargetPaths value (rooted per-file paths verbatim; folder + mask/name combined). * ComposeRowDisplayText(baseLabel, isSharedFolder, sharedSuffix) - the display label plus the "(from SharedOutputFiles)" provenance suffix. - fmImport.cs (#16): AddFilesFromDirectory now calls the two helpers instead of composing the path/label inline, so production and tests share one implementation. No behavior change. - SharedOutputFolderTests.cs (#16, #17): add 13 tests - ComposeRowTargetPath (rooted verbatim, folder+mask, folder+bare-name, empty base, empty leaf, UNC), ComposeRowDisplayText (primary/shared/empty-suffix/null-label), and edge cases: quoted input, case-insensitive folder name, and a UNC instance path. Full suite now 294 passing. - TestingInfrastructure/README.md (#18): fix factual errors - list all five ProjectReferences (was two) and replace the obsolete InternalsVisibleTo("SqlNexus.UnitTests") guidance with the strong-name-qualified (PublicKey=...) form now required, since the test project is strong-named. - Help/QuickStartPostmortemAnalysis.htm (#19): add a user-facing note that SQL Nexus also scans the sibling SharedOutputFiles folder, that rows may appear for files outside the selected folder (labeled "(from SharedOutputFiles)"), and that files present in both folders are imported only once. No import-logic behavior change; build clean; 294 unit tests pass.
RawFileImporter was extended to scan both the primary instance folder and the sibling SharedOutputFiles folder, but unlike the other importers it had no duplicate-name guard. Because ImportFile does a plain INSERT per file, a raw file present in BOTH folders (e.g. a host/OS .out copied into both) was imported twice into the same table, producing silent duplicate rows. Fix: for the sibling folder, filter out any file whose name was already imported from the primary folder via the existing (unit-tested) SharedOutputFolder .GetSiblingOnlyFiles helper. The primary copy wins and the skipped duplicate is logged with the same "Shared folder: skipping duplicate file ..." message used by the other importers. When no sibling folder exists, behavior is unchanged. Build clean; 294 unit tests pass.
The pure-string tests for ComposeRowTargetPath, FilterDuplicateSiblingFiles, and GetSiblingOnlyFiles used hardcoded D:\ literals. These helpers only do string operations (no filesystem access), so the tests did not actually fail on machines without a D: drive - but hardcoding a non-guaranteed drive letter is poor hygiene and misleading. Switch all such literals to C:, the only drive that can be assumed. Filesystem-touching tests already use Path.GetTempPath() and were unchanged; UNC (\\server\share) test paths were left intact. No production change; 294 unit tests pass.
Review — PR #558 · Import sibling
|
| Description says | Code actually does |
|---|---|
| "validates the sibling is a real direct sibling … rejects traversal" | the direct-sibling guard was removed; .. is normalised and accepted |
| "Mask-based importers … get a second row pointing at the sibling" | the sibling row is skipped when the primary already matched |
"SqlNexus.UnitTests: SignAssembly=false" / "ErrorLogImporter: remove key-qualified InternalsVisibleTo and widen 6 members to public" |
test project stays signed; the key-qualified InternalsVisibleTo was added; the members stay internal |
| Fix: rewrite the description to describe the final commits. |
🟡 Root README.md not updated. It's the front door and already points at SQL LogScout.
Fix: add two lines explaining the SharedOutputFiles behaviour and the (from SharedOutputFiles)
row suffix, so users can decode what they see in the UI.
🟡 Confirm the strong-naming change is intended and safe.
sqlnexus.csproj flips SignAssembly false → true for the shipping EXE, and
SqlNexus.McpServer.csproj gains strong naming. That's a packaging change well beyond "import a
sibling folder". The csproj comment claims "a strong-named test assembly cannot load an unsigned
reference at runtime (FileLoadException)" — that isn't generally true on .NET Framework (CS8002 is
only a warning, and it's already in NoWarn).
Fix: confirm the EXE genuinely needed signing rather than just the key-qualified
InternalsVisibleTo, and confirm the installer / release-signing pipeline is unaffected. If it
wasn't required, revert it to keep this PR focused.
9. Do we have proper test suite coverage?
- Big win, and worth calling out: fixing the signing/
InternalsVisibleTosetup re-enabled the
existing sqlnexus tests in the command-line runner. That's what surfaced theIsDbNameValid
crash — thenull/""/" "cases inDatabaseCommandTextTests.cshad existed for a while but
had never actually executed. Good catch. - 🟡 Coverage gap on the orchestration code — see item 5.
- ⚪ "170/170 passing" couldn't be independently verified here —
dotnet testcan't restore in
this environment (Microsoft.SqlServer.XEvent.XELitereturns HTTP 402 from the private feed, a
known long-standing issue unrelated to this PR).
Fix: attach the test-run output or a CI link to the PR.
10. Is the GUI updating consistently with other elements in the same section?
- Good: the banner is wired to
tbPath_TextChanged— the same handler that enables the Import
button — so it updates in lockstep with the rest of the path section. - 🟡 It doesn't follow the app's theming, unlike its neighbours. Every other control in
paTop(laPath,laInstructions,llOptions,cbPath) is painted byThemeManager.ApplyTheme.
The new label hard-codesSystemColors.Info/InfoTextand deliberately re-applies them over
the theme on every path change. In the Aquatic dark theme (#202020) this will read as a
foreign pale-yellow strip; same mismatch in Desert.
Fix: take the colours fromThemeManagerfor the three themes, and bypass toSystemColorsonly
whenSystemInformation.HighContrastis on — matchingThemeManager.ApplyHighContrastTheme(). - 🔴 It also doesn't follow the panel's layout model. Everything else in
paTopis anchored;
this label is absolutely positioned at(0, 63)where it overlaps the Options link and the
Import/Close buttons, and only ends up in the right place thanks to the runtime+22pxfudge —
which is the high-DPI bug in item 1.
Fix: dock it to the bottom ofpaTop(or letpaTopauto-size) so it flows like everything else.
Suggested merge order
- 🔴 High-DPI layout / dock the banner properly (items 1 & 10)
- 🔴 Guard the folder enumeration against permission/path errors (item 1)
- 🔴 Put the user documentation somewhere users can actually read it (item 7)
- 🔴 Refresh the PR description to match the code (item 7)
- 🟡 Everything else above
- ⚪ Nits — fine to batch into a follow-up
Also worth a separate mention in the description: this branch deletes sqlnexus/fmnexus.cs.2
(~3,160 lines, an old backup copy). It's a good cleanup, just unrelated to the feature.
Appendix A — Detailed issue context
Line-level detail, evidence and repro steps for every item in the checklist above.
🔴 B1. Banner covers the Import/Close buttons at non-100% display scaling
Where: sqlnexus/fmImport.cs, sqlnexus/fmImport.Designer.cs
What the code does now
The new laSharedFolder label is placed by the designer at a fixed spot, Location = (0, 63),
Size = 455 x 18, anchored Bottom | Left | Right. At that position it physically sits on top of
three existing controls in the same panel:
| Control | Location | Size | Occupies Y |
|---|---|---|---|
llOptions ("Options" link) |
(9, 66) | 43 x 13 | 66–79 |
tsbGo ("Import" button) |
(295, 56) | 75 x 25 | 56–81 |
btnClose ("Close" button) |
(376, 56) | 75 x 25 | 56–81 |
laSharedFolder |
(0, 63) | 455 x 18 | 63–81 ← overlaps all three |
It only appears correct because ShowSharedFolderLabel manually grows the panel and the form by a
hard-coded constant, which pushes the bottom-anchored label down out of the way:
private const int SharedFolderLabelRowHeight = 22; // line 2361
int delta = show ? SharedFolderLabelRowHeight : -SharedFolderLabelRowHeight;
paTop.Height += delta;
this.ClientSize = new Size(this.ClientSize.Width, this.ClientSize.Height + delta);Why it breaks
fmImport sets AutoScaleMode = AutoScaleMode.Font with AutoScaleDimensions = 6F, 13F
(fmImport.Designer.cs). At runtime WinForms scales every
designer coordinate and size by the user's DPI / font setting — but a constant written in C# code
is not scaled. So:
| Display scaling | Label height after scaling | Panel grows by | Result |
|---|---|---|---|
| 100% | 18 px | 22 px | OK (4 px clearance) |
| 125% | ~23 px | 22 px | Label overlaps the buttons by ~1 px |
| 150% | ~27 px | 22 px | Label overlaps the buttons by ~5 px |
| 200% | ~36 px | 22 px | Label covers the button row |
Repro: Windows Settings → Display → Scale = 150% → sign out/in → open SQL Nexus → Import →
point at a LogScout instance folder that has a SharedOutputFiles sibling. The yellow banner is drawn
over the Import and Close buttons.
Expected: the banner always sits below the button row at any scaling, and no manual pixel maths is
needed.
Secondary defect in the same method: the growth is never reversed when the dialog expands into the
import view — fmImport.cs sets m_sharedFolderLabelShown = false
and hides the label but leaves paTop.Height 22 px taller, so the header keeps a dead gap.
🔴 B2. An unreadable SharedOutputFiles folder aborts the whole import scan
Where: sqlnexus/fmImport.cs, sqlnexus/fmImport.cs, sqlnexus/FileImporter.cs
What the code does now
// fmImport.cs:307 — inside AddFilesFromDirectory, no try/catch
string[] allMatches = Directory.GetFiles(basePath, Mask);// fmImport.cs:1301 — the call site, also no try/catch
foreach (string s in prod.SupportedMasks)
{
if (AddFiles(s, prod))
anyFilesFound = true;
}// FileImporter.cs:75 — RawFileImporter.DoImport, same pattern
string[] files = Directory.GetFiles(searchPath, rawfile.Mask);Why it's worse than before this PR
Before the change there was exactly one folder to enumerate, and it was the one the user explicitly
browsed to and which tsbGo.Enabled = Directory.Exists(cbPath.Text) had already validated. Now a
second, implicitly discovered folder is enumerated with the same unguarded call. Directory.Exists
returning true does not guarantee GetFiles will succeed — it can still throw:
UnauthorizedAccessException— the folder or a file in it has restrictive ACLs (common when a
capture is copied off a locked-down production box, or unzipped by a different user).PathTooLongException— the sibling path plus a long LogScout filename exceedsMAX_PATHon
.NET Framework 4.8.IOException— the folder is on a network share that dropped mid-scan.
Any of these propagates out of AddFiles → out of the importer enumeration loop → to the global
handler. The user gets a crash dialog and zero files imported, including everything in the
folder they actually chose.
Repro: create output\SharedOutputFiles, then icacls output\SharedOutputFiles /deny "%USERNAME%":(R).
Point Nexus at output\SERVER_SQL2019 and press Import.
Expected: the sibling folder is skipped with a visible warning; the primary folder still imports
normally.
🔴 D1. The user documentation lands in a file nobody reads
Where: sqlnexus/Help/QuickStartPostmortemAnalysis.htm
The PR adds a well-written 15-line explanation of the new behaviour to that page. Verified facts:
QuickStartPostmortemAnalysis.htmdoes not appear in the[FILES]section of
sqlnexus/Help/sqlnexus2.hhp. That list is only:UsingSQLNexus.htm(default topic),
AddingNewReports.htm,CollectingDiagnostics.htm,CustomizingReports.htm,
EmailingReports.htm,ExportingReports.htm,Overview.htm,RunningReports.htm.- No other help page links to it (a repo-wide search for the filename returns only the file's own
self-referencing<link rel=File-List>tag). - It is not a
Contentitem insqlnexus.csproj, so it isn't even copied to the output folder. sqlnexus.chmis a checked-in binary that only changes when someone recompiles from the
.hhp. This PR does not touch the.chm.
Net effect: the text exists only in the git repo. Pressing F1 in the app will never show it.
Expected: the note reaches users — either in a compiled help topic, or in the repo README.md /
the project wiki, which is where the rest of the user-facing guidance actually lives.
🔴 D2. The PR description contradicts the shipped code in three places
Reviewers, release notes and future git archaeology all read the description first. Three of its
claims are now the opposite of what the branch contains:
| Description claims | What commit 536ab97 actually does |
Evidence |
|---|---|---|
| "validates the sibling is a real direct sibling … rejects traversal, fails closed" | GetImportSearchPaths runs Path.GetFullPath first, which collapses ..; the direct-sibling guard was deleted |
SharedOutputFolderTests.GetImportSearchPaths_PrimaryContainsDotDotSegment_NormalizesAndResolvesSibling asserts the traversal path succeeds, and its own comment says "after removing the (dead) direct-sibling guard" |
| "Mask-based importers (e.g. Perfmon BLG) get a second row pointing at the sibling" | The sibling row is skipped whenever the primary folder already produced a row | fmImport.cs — if (isShared && anyAdded && !(Importer is INexusFileImporter)) continue; (added later, in commit 3f16ee9) |
| "SqlNexus.UnitTests: SignAssembly=false" and "ErrorLogImporter: remove key-qualified InternalsVisibleTo and widen 6 members from internal to public" | The test project stays SignAssembly=true (now using sqlnexus\SqlNexus.snk); the key-qualified InternalsVisibleTo was added; the ErrorLogImporter members remain internal |
SqlNexus.UnitTests.csproj, ErrorLogImporter/Properties/AssemblyInfo.cs, ErrorLogImporter/ErrorLogImporter.cs |
The description was clearly accurate for the first commit and was never updated as review feedback
reshaped the branch across the following 10 commits. (For the record, the final signing approach is
the better one — it keeps the product internals internal.)
🟡 B3. LinuxPerfImporter working directory is a global that is never restored
Where: sqlnexus/fmImport.cs, sqlnexus/fmImport.cs
LinuxPerfImporter.Model.ConfigValues.WorkingDirectory is a static property. It's set once before
the import loop:
LinuxPerfImporter.Model.ConfigValues.WorkingDirectory = srcPath; // line 1539then re-pointed inside the loop for a Linux-perf row that came from the sibling folder:
if (ri.Name != null && ri.Name.IndexOf("Linux Performance", StringComparison.OrdinalIgnoreCase) >= 0)
{
string targetDir = Path.GetDirectoryName(targetPath);
if (!string.IsNullOrEmpty(targetDir))
LinuxPerfImporter.Model.ConfigValues.WorkingDirectory = targetDir; // line 1658
}There is no corresponding restore. Once a shared-folder Linux-perf row runs, the global points at
...\SharedOutputFiles for the remainder of the process — including any later import, post-processing
step, or a second Import run in the same session that reuses the cached value before line 1539 runs
again. The bug is latent today but is exactly the kind that surfaces months later as "the second
import in a session reads the wrong folder".
Expected: scope the change to the row, e.g. capture the previous value and restore it in a
finally.
🟡 L1. Files are dropped where the user cannot see it
Where: sqlnexus/fmImport.cs
if (isShared && anyAdded && !(Importer is INexusFileImporter))
{
MainForm.LogMessage(
"Shared folder: skipping duplicate files matching '" + Mask + "' ...",
MessageOptions.Silent); // ← file-only
continue;
}For mask-based importers (Perfmon *.blg, ReadTrace *.trc/*.xel) this discards every matching
file in SharedOutputFiles as soon as the instance folder produced one match. The reasoning in the
comment is sound (a second row would re-run table setup over the first run's results), but
MessageOptions.Silent writes only to sqlnexus.log. A user whose Perfmon counters are split across
both folders sees a successful import with half the data and no indication why.
Expected: this is a data-completeness decision, not a debug detail. Surface it at
MessageOptions.All (status bar + log + dialog) with the folder name and the count of skipped files.
🟡 L2. The most important warning is effectively invisible
Where: sqlnexus/fmImport.cs
The "same name, different size" case is genuinely ambiguous — the two files may be different
captures, and the code deliberately refuses to guess. Its own comment says it should be "surfaced
clearly so the user can decide". But it is logged as:
MainForm.LogMessage("... WARNING: it exists in BOTH ... with a DIFFERENT size ...",
MessageOptions.Both);and in NexusInterfaces/interfaces.cs:
StatusBar = 2,
Silent = 4,
Dialog = 8,
Both = StatusBar | Silent, // ← no Dialog
All = StatusBar | Silent | Dialog,Both = status bar + log file only. It is emitted once per colliding file inside a tight loop, so
each message overwrites the previous one within milliseconds. In practice the user sees a flicker and
then whatever the next log line is.
Expected: collect the ambiguous collisions, then emit one MessageOptions.All message at the
end of the scan listing the count and the file names. Keep the per-file detail at Silent.
🟡 A1 / A2. Accessibility gaps on the new banner
Where: sqlnexus/fmImport.cs
Two distinct problems:
A1 — screen reader users are never told the second folder was added. laSharedFolder is a plain
Label: it is not focusable, not in the tab order, and has no UIA live-region semantics. It is
populated from tbPath_TextChanged, i.e. while the user is typing in cbPath. Focus never leaves
cbPath, so NVDA/Narrator/JAWS have no reason to read the label and the user is simply not informed
that a second folder will be scanned. The AccessibleName is set correctly, but nothing ever asks
for it.
Fix direction: mirror the message into cbPath.AccessibleDescription (that control is focused,
so it will be announced), or raise a UIA notification.
A2 — the full path is mouse-only. The label shows an abbreviated path
(AbbreviatePath(sibling, 48) → C:\...\output\SharedOutputFiles) plus AutoEllipsis; the complete
path exists only in toolTip1. Tooltips in WinForms appear on mouse hover and are not reachable
by keyboard, so a sighted keyboard-only user cannot read the full path (WCAG 2.1.1 Keyboard).
Fix direction: also write the full path to the log at a user-visible level, or let the label wrap /
show the full path when the dialog is wide enough.
A3 (nit) — stale accessible name. In the else branch the visible Text and the tooltip are
cleared, but AccessibleName still holds the previous folder's full path, so a screen reader can read
out a folder that is no longer being scanned.
🟡 T1. The behaviour that actually changed has no tests
SharedOutputFolderTests.cs (~30 tests) is good work, but every test targets a pure helper.
The three pieces of logic that determine what actually gets imported are untested because they are
embedded in WinForms / SQL code:
| Untested logic | Where | Why it matters |
|---|---|---|
| "skip the sibling when the primary already matched" for mask importers | fmImport.cs | Decides whether Perfmon data is dropped (L1) |
Two-folder loop + name dedupe in RawFileImporter.DoImport |
FileImporter.cs | Decides whether raw files are double-imported |
m_RowTargetPaths lookup + legacy srcPath + ll.Text fallback |
fmImport.cs | Decides which file each importer actually opens |
The PR already demonstrates the right technique — SharedOutputFolder was extracted specifically so
it could be tested without WinForms. Apply the same technique once more to these three.
🟡 S1. Strong-naming the shipping EXE is out of scope and rests on a questionable claim
Where: sqlnexus/sqlnexus.csproj, SqlNexus.McpServer/SqlNexus.McpServer.csproj
sqlnexus.csproj flips SignAssembly from false to true — the main product EXE is now
strong-named — and SqlNexus.McpServer.csproj gains strong naming too. The justification in the
csproj comment is:
A strong-named test assembly cannot load an unsigned reference at runtime (FileLoadException), and a
signed assembly can only grant InternalsVisibleTo to a strong-named friend.
The second half is correct and is the real reason the key-qualified InternalsVisibleTo was
needed. The first half is not generally true on .NET Framework: referencing an unsigned assembly
from a strong-named one produces compiler warning CS8002, which this project already suppresses
via <NoWarn>$(NoWarn);8002</NoWarn>, and it loads fine at runtime. If that's right, signing the EXE
was not required to unblock the tests.
Why it matters: strong-naming a shipping executable is a packaging change with downstream effects
(installer/WiX, the release signing pipeline, anything that binds by full assembly name). It deserves
its own PR or at least an explicit "yes, we want this" from the maintainer — not a side effect of a
folder-scanning feature.
⚪ Nits, with locations
| # | Item | Location |
|---|---|---|
| N1 | Missing primary folder now returns "no files" instead of an error — add a log line | fmImport.cs |
| N2 | "Number of files blocked for import…: 0" now logged per-mask even when nothing matched |
fmImport.cs |
| N3 | Path.GetFullPath + a second Directory.Exists run on every keystroke in the path box |
fmImport.cs |
| N4 | MainForm is not null-checked inside the catch, so an error could throw out of the error handler |
fmImport.cs |
| N5 | Bare catch { } with no exception variable and no log (repo convention violation) |
fmImport.cs |
| N6 | Comment says the suffix is "(SharedOutput)"; the real suffix is " (from SharedOutputFiles)" |
fmImport.cs |
| N7 | Closing brace still marked //end of AddFiles — the method is now AddFilesFromDirectory |
fmImport.cs |
| N8 | AddFileRow is now a one-line wrapper over AddFileRowReturningLabel |
fmImport.cs |
| N9 | New README lines contain a raw Windows-1252 em dash (byte 0x97) → renders as � on GitHub |
TestingInfrastructure/README.md |
| N10 | Loud log messages embed full folder paths (host/server names); prefer names + counts at All, full paths at Silent |
fmImport.cs |
Appendix B — Copy-paste prompt for the AI
Paste everything in the block below into Copilot / your agent of choice, with the
ImportSharedOutputDir_556_pijocoder_090326branch checked out.
You are working in the SqlNexus repo (C# 7.3, .NET Framework 4.8, WinForms) on branch
ImportSharedOutputDir_556_pijocoder_090326. Follow .github/copilot-instructions.md at all times:
minimal focused edits, no reformatting of untouched code, no new build warnings, every behavior
change covered by an MSTest unit test under
TestingInfrastructure/UnitTests/SqlNexus.UnitTests/, and no empty catch blocks.
Apply the following fixes from code review of PR #558. Work through them in order. After each
group, build sqlnexus.csproj and report any errors.
=== GROUP 1 — BLOCKERS ===
1. Fix the high-DPI layout break in the "Also scanning:" banner.
Files: sqlnexus/fmImport.Designer.cs, sqlnexus/fmImport.cs (ShowSharedFolderLabel ~line 2360).
Problem: laSharedFolder is absolutely positioned at (0,63) where it overlaps llOptions (9,66),
tsbGo (295,56) and btnClose (376,56). It only looks right because ShowSharedFolderLabel adds a
hard-coded 22 px to paTop.Height and this.ClientSize. The form uses AutoScaleMode.Font, so the
label scales with DPI but the 22 does not — at 125%+ the banner covers the Import/Close buttons.
Required fix: remove the SharedFolderLabelRowHeight constant and the manual pixel arithmetic.
Make the layout scale-independent instead — dock laSharedFolder to the bottom of paTop (or place
it in an auto-sizing container row) and move it below the button row in the designer so it never
overlaps. If a runtime resize is still unavoidable, compute the delta from
laSharedFolder.PreferredHeight (or .Height) at runtime, never from a literal.
Also fix the related defect at fmImport.cs ~line 1428: when the dialog expands into the import
view it sets m_sharedFolderLabelShown = false and hides the label but never reverses the panel
growth, leaving a dead gap in the header.
Verify: the banner sits fully below the Import/Close buttons at 100%, 125%, 150% and 200%
display scaling, and the header has no leftover gap after the dialog expands.
2. Stop an unreadable SharedOutputFiles folder from aborting the entire import.
Files: sqlnexus/fmImport.cs (AddFilesFromDirectory ~line 307), sqlnexus/FileImporter.cs
(RawFileImporter.DoImport ~line 75).
Problem: Directory.GetFiles is called with no try/catch, and the call site fmImport.cs ~line 1301
(foreach over prod.SupportedMasks) is unguarded too. Directory.Exists returning true does not
guarantee GetFiles succeeds — UnauthorizedAccessException, PathTooLongException and IOException
are all reachable for the implicitly-discovered sibling folder. Today any of them crashes the
whole enumeration and nothing at all gets imported.
Required fix: wrap each per-directory enumeration in try/catch for UnauthorizedAccessException,
PathTooLongException and IOException (catch the specific types, not bare Exception). Log with
MainForm.LogMessage(..., MessageOptions.All) naming the folder and the reason, then continue so
the primary folder still imports. Do the same in RawFileImporter.DoImport using
Util.Logger.LogMessage.
Verify: with `icacls output\SharedOutputFiles /deny "%USERNAME%":(R)` applied, importing
output\SERVER_SQL2019 still succeeds and shows a clear warning about the skipped folder.
3. Put the user documentation where users can actually read it.
Problem: sqlnexus/Help/QuickStartPostmortemAnalysis.htm is an orphan — it is NOT in the [FILES]
list of sqlnexus/Help/sqlnexus2.hhp, no other help page links to it, and it is not a Content item
in sqlnexus.csproj. sqlnexus.chm is a checked-in binary that only changes when the .hhp is
recompiled, and this PR does not touch it. The new text therefore reaches no user.
Required fix: move the SharedOutputFiles explanation into a help topic that IS compiled — best
candidates are sqlnexus/Help/UsingSQLNexus.htm (the default topic) or
sqlnexus/Help/CollectingDiagnostics.htm. Keep the wording and the existing HTML style of the
target page. IMPORTANT: those files are plain ASCII while WordSources/*.htm are Windows-1252 —
do not let the editor re-encode anything; after saving, confirm no byte > 127 was introduced that
was not there before. Note in the PR description that sqlnexus.chm must be recompiled from
Help/sqlnexus2.hhp for the change to ship.
Also add a short (2–4 line) note to the repo root README.md explaining the SharedOutputFiles
behavior and the "(from SharedOutputFiles)" row suffix, since that is the front door for users.
=== GROUP 2 — SHOULD FIX ===
4. Scope the LinuxPerfImporter working-directory change to the row that needs it.
File: sqlnexus/fmImport.cs ~lines 1646-1660.
Problem: LinuxPerfImporter.Model.ConfigValues.WorkingDirectory is a static global. It is
re-pointed at the sibling folder for a Linux-perf row and never restored, so it stays wrong for
everything that runs afterwards in the process.
Required fix: capture the previous value before assigning and restore it in a finally block
around that row's Initialize/DoImport.
5. Make dropped files visible to the user.
File: sqlnexus/fmImport.cs ~lines 269-281.
Problem: for mask-based importers (Perfmon *.blg, ReadTrace) every matching file in
SharedOutputFiles is discarded once the primary folder matched, and this is logged only at
MessageOptions.Silent. A user whose Perfmon data spans both folders silently loses half of it.
Required fix: raise this to MessageOptions.All and include the skipped folder path and the count
of skipped files.
6. Make the ambiguous-duplicate warning actually reach the user.
File: sqlnexus/fmImport.cs ~lines 336-348.
Problem: the "same name but DIFFERENT size" warning uses MessageOptions.Both, which is
StatusBar|Silent only (see NexusInterfaces/interfaces.cs) — no dialog — and it is emitted once
per file in a tight loop, so each message instantly overwrites the previous one.
Required fix: accumulate the ambiguous collisions during the scan, then emit ONE
MessageOptions.All message at the end containing the count and the file names. Keep the per-file
detail at MessageOptions.Silent.
7. Fix the accessibility gaps on the banner.
File: sqlnexus/fmImport.cs, UpdateSharedFolderAffordance ~lines 2287-2326.
(a) laSharedFolder is a non-focusable Label updated while focus is in cbPath, so a screen reader
never announces that a second folder will be scanned. Mirror the message into
cbPath.AccessibleDescription (that control IS focused) so it is announced, or raise a UIA
notification.
(b) The full sibling path exists only in toolTip1, which is mouse-only (WCAG 2.1.1 Keyboard).
Make the full path reachable without a mouse — e.g. also log it at a user-visible level
and/or let the label show the full path when width allows.
(c) In the else branch, clear laSharedFolder.AccessibleName (and AccessibleDescription) as well
as Text and the tooltip, so a stale folder path is not read out.
8. Theme the banner like the rest of the panel.
File: sqlnexus/fmImport.cs ~lines 2305-2310, sqlnexus/fmImport.Designer.cs.
Problem: every other control in paTop (laPath, laInstructions, llOptions, cbPath) is painted by
ThemeManager.ApplyTheme. The banner hard-codes SystemColors.Info/InfoText and deliberately
re-applies them OVER the theme on every path change, so in the Aquatic dark theme (#202020) it is
a foreign pale-yellow strip, and it clashes in Desert too.
Required fix: derive the banner colors from ThemeManager (ThemeManager.CurrentThemeName,
CurrentForeColor, CurrentBackColor, CurrentOtherColor) for the Default/Aquatic/Desert themes, and
fall back to SystemColors ONLY when ThemeManager.IsHighContrastEnabled is true — matching the
pattern in ThemeManager.ApplyHighContrastTheme. Keep the "Also scanning:" wording so meaning is
never carried by color alone, and keep contrast within the theme's own palette.
9. Add unit tests for the behavior that actually changed.
Problem: all ~30 new tests target pure helpers; the three pieces of logic that decide what gets
imported are untested because they are embedded in WinForms/SQL code.
Required fix: apply the same extraction technique the PR already used for SharedOutputFolder, and
add tests in TestingInfrastructure/UnitTests/SqlNexus.UnitTests/sqlnexus/ for:
- the "skip the sibling when the primary already matched" rule for mask-based importers
(extract a pure predicate such as ShouldSearchSiblingFolder(bool importerIsPerFile,
bool primaryAlreadyAdded) from fmImport.AddFiles ~line 269);
- the two-folder loop + name-dedupe in RawFileImporter.DoImport (extract the folder/mask
selection so it can be exercised without a SQL connection);
- the m_RowTargetPaths lookup including the legacy `srcPath + ll.Text` fallback path.
Cover happy path, boundary cases and negative cases for each, per copilot-instructions.md.
10. Justify or revert strong-naming the shipping EXE.
Files: sqlnexus/sqlnexus.csproj, SqlNexus.McpServer/SqlNexus.McpServer.csproj.
Problem: SignAssembly was flipped false -> true for the main product EXE (and added to the MCP
server) as a side effect of unblocking the tests. The csproj comment claims "a strong-named test
assembly cannot load an unsigned reference at runtime (FileLoadException)" — that is not
generally true on .NET Framework; it produces warning CS8002, which is already suppressed via
<NoWarn>$(NoWarn);8002</NoWarn>.
Required action: empirically verify whether the tests pass with sqlnexus.csproj SignAssembly
back at false while keeping the key-qualified InternalsVisibleTo. If they do, revert the
SignAssembly changes to keep this PR focused. If they do not, correct the csproj comment to
state the real reason and flag the packaging/installer/signing-pipeline impact in the PR
description.
=== GROUP 3 — NITS ===
11. fmImport.cs ~304: log a warning (MessageOptions.All) when the PRIMARY import folder does not
exist, instead of silently returning "no files found".
12. fmImport.cs ~288: only emit "Number of files blocked for import…" when blockedCounter > 0.
13. fmImport.cs ~2278: tbPath_TextChanged now runs Path.GetFullPath plus a second Directory.Exists
on every keystroke, which blocks the UI thread on an unreachable UNC path. Only recompute when
the text is a rooted, plausible path, or debounce.
14. fmImport.cs ~2319: use MainForm?.LogMessage inside the catch so an error cannot throw out of the
error handler.
15. fmImport.cs ~2350: replace the bare `catch { }` in AbbreviatePath with
`catch (Exception ex)` plus a MessageOptions.Silent log, per copilot-instructions.md.
16. fmImport.cs ~297: the comment says the suffix is "(SharedOutput)"; the real suffix is
" (from SharedOutputFiles)". Fix the comment.
17. fmImport.cs ~440: the closing brace still says "//end of AddFiles" but the method is now
AddFilesFromDirectory. Fix the marker.
18. fmImport.cs ~442: AddFileRow is now a one-line wrapper over AddFileRowReturningLabel — call the
new method directly and delete the wrapper.
19. TestingInfrastructure/README.md ~50: the newly added lines contain a raw Windows-1252 em dash
(byte 0x97) which renders as U+FFFD on GitHub. Replace with "--" or a proper UTF-8 em dash. Do
not disturb the pre-existing 0x97 bytes elsewhere in the file unless you convert the whole file
deliberately.
20. Trim the block comments that only restate what the next few lines do (keep the ones that explain
WHY — e.g. why the primary copy wins, why the sibling is not scanned for Custom XEL, why the
Linux importer is matched by name). Per copilot-instructions.md, comments should state what the
code cannot show on its own.
=== FINALLY ===
21. Rewrite the PR description to match the code as it now stands. Three statements are currently the
opposite of what shipped:
- it claims traversal is rejected, but Path.GetFullPath collapses ".." and the direct-sibling
guard was removed (see the test
GetImportSearchPaths_PrimaryContainsDotDotSegment_NormalizesAndResolvesSibling);
- it claims mask-based importers get a second row for the sibling, but the code skips the
sibling when the primary already matched;
- it claims SqlNexus.UnitTests uses SignAssembly=false and that ErrorLogImporter's
key-qualified InternalsVisibleTo was removed with 6 members widened to public — the final
code does the opposite (test project stays signed, key-qualified IVT added, members stay
internal).
Also mention that this branch deletes sqlnexus/fmnexus.cs.2 (~3,160 lines, an old backup copy) —
a good cleanup, but unrelated to the feature.
22. Run the full unit test suite and paste the result (or a CI link) into the PR. Note: in some
environments `dotnet test` cannot restore because Microsoft.SqlServer.XEvent.XELite returns
HTTP 402 from the private feed — that is a known feed/billing issue unrelated to this PR.
…ration Two robustness fixes for the shared-folder import affordance and scan. - fmImport.cs (High-DPI layout): ShowSharedFolderLabel grew the header by a hard-coded 22 pixels while the form uses AutoScaleMode.Font, so at 125%/150% display scaling the "Also scanning" banner overlapped the Import/Close buttons. Grow the top panel and form by the label's runtime height (PreferredHeight + vertical margin) instead of a fixed constant, and remove exactly the pixels added (tracked in m_sharedFolderLabelDelta) when hiding, so the layout is correct at any DPI. - fmImport.cs / FileImporter.cs (resilient enumeration): AddFilesFromDirectory and RawFileImporter.DoImport called Directory.GetFiles with no try/catch. A sibling SharedOutputFiles folder with restrictive permissions or a too-long path threw UnauthorizedAccessException / PathTooLongException and aborted the entire import scan - for a folder the user never selected. Wrap each per-directory enumeration in catch (UnauthorizedAccessException | IOException | PathTooLongException), log with MessageOptions.All, and continue with the remaining folders. The primary folder import proceeds unaffected. Build clean; 294 unit tests pass.
…ity, layout, perf) Five robustness/UX fixes from review feedback, all in the import dialog. - Restore LinuxPerfImporter working directory. ConfigValues.WorkingDirectory is a static global; a per-row repoint (e.g. to the sibling SharedOutputFiles folder) leaked into every importer that ran afterwards. Save it before each row and restore it in a finally. - Make dropped mask-based data visible. For aggregating importers (Perfmon .blg, ReadTrace) the sibling folder is skipped entirely when the primary already matched the mask. That skip was logged only at MessageOptions.Silent, so files present in both folders disappeared silently. Log it at MessageOptions.All, name the skipped folder, and tell the user to import it separately. - Warn on a missing primary source folder. A mistyped/moved primary path fell through !Directory.Exists as "no files found". Log a MessageOptions.All warning when the primary folder is missing (a missing sibling stays silent - that is normal), deduped to once per folder per run. - Remove dead header space after expand. When the form expands into the import view, reverse the compact-form header growth (paTop.Height) that was added for the "Also scanning" label instead of only clearing the tracking flag. - Avoid path work on every keystroke. tbPath_TextChanged only recomputes the shared-folder affordance (GetFullPath + sibling Directory.Exists) when the typed path is rooted and already exists, reusing the existing existence check, so a partially typed / unreachable path does not trigger extra probing per character. Build clean; 294 unit tests pass.
Logging and accessibility refinements from review feedback (import dialog).
Logging
- Make the important warning visible. The "same name, DIFFERENT size" case (a file
NOT imported) was logged with MessageOptions.Both, so the only user-visible part
was a status-bar line that is overwritten instantly. Aggregate all such
collisions into ONE MessageOptions.All (dialog) message with a count and the file
names; keep full paths in the log at Silent. The low-risk same-name-and-size case
drops from Both to Silent.
- Remove log noise. "Number of files blocked for import ...: 0" is now logged only
when blockedCounter > 0, not once per mask for every file importer.
Accessibility
- Announce the second folder to screen readers. laSharedFolder is a non-focusable
Label, so mirror the announcement onto cbPath.AccessibleDescription (a focusable
control the user lands on).
- Make the full path keyboard-reachable (WCAG 2.1.1). It was mouse-only via the
tooltip; also log the full sibling path at MessageOptions.All the first time each
sibling is detected (guarded so it does not repeat per keystroke).
- Clear stale accessible state. The else branch now clears laSharedFolder
.AccessibleName and cbPath.AccessibleDescription when the sibling disappears.
Error handling
- Guard MainForm in UpdateSharedFolderAffordance's catch (MainForm?.LogMessage) so
an error on the parameterless-constructor path cannot throw out of the handler.
- Replace the bare catch {} in AbbreviatePath with catch (Exception ex) + a Silent
log, per repo convention.
Build clean; 294 unit tests pass.
…doc/comment fixes Review round: structural layout fix, testable orchestration helper, and doc cleanups. Layout (fmImport.Designer.cs, fmImport.cs) - Dock the "Also scanning" banner to the bottom of paTop (Dock=Bottom, AutoSize) instead of absolutely positioning it at (0,63) where it overlapped the Options link and Import/Close buttons. It now flows with the panel like every other control. The compact form still grows/shrinks by the banner's own measured height (DPI-safe), and the runtime-height logic is simplified to read the docked label's actual laid-out height. Testable orchestration (SharedOutputFolder.cs, fmImport.cs, SharedOutputFolderTests.cs) - Extract the mask-based "skip the sibling when the primary already matched" rule into a pure ShouldSkipSiblingForMask helper and unit-test it (mask importer + primary matched => skip; primary empty => scan; per-file importer => never skip; primary folder => never skip). 40 SharedOutputFolder tests pass. Comments / docs - Fix stale doc comment: row suffix is " (from SharedOutputFiles)", not "(SharedOutput)". - Fix mislabeled closing-brace marker: //end of AddFilesFromDirectory. - TestingInfrastructure/README.md: replace raw Windows-1252 em dashes (0x97, which GitHub renders as the replacement char) with ASCII "--". - README.md: add a short "Importing SQL LogScout All Instances captures" section explaining the SharedOutputFiles behavior and the (from SharedOutputFiles) suffix so users can decode what they see in the UI. Note: AddFileRow was NOT removed - it still has 6 live callers (it is a convenience overload that discards the returned label), so it is not dead code. Build clean; 294 unit tests pass.
When a mask-based importer (e.g. Perfmon .blg) finds files in the primary folder, the sibling SharedOutputFiles copies are intentionally not re-imported. That skip was surfaced as a modal MessageOptions.All dialog reading "NOT importing files matching ...", which looked like an error even though the files WERE imported from the primary folder (confirmed by a real import test). - Reword the message to lead with what happened: "Files matching '*.BLG' were imported from the primary folder. Additional matching files in '<shared>' were not also imported (to avoid re-processing the same data) ... import that folder separately." - Drop it from a modal dialog (MessageOptions.All -> Both): log file + status bar, so it stays discoverable without interrupting the user. This is safe because the guard only skips the sibling when the primary already imported those files, so the data is always imported from somewhere. Build clean; 298 unit tests pass.
… open Opening the Import form seeds cbPath with the remembered import path (Settings.Default.ImportPath), which fires tbPath_TextChanged and raised the "A sibling shared folder was detected..." dialog (MessageOptions.All) with no user action - confusing when the last-used folder happened to have a SharedOutputFiles sibling. Separate the visual banner from the loud dialog: - Add an m_suppressAffordanceDialog guard set around the two programmatic cbPath.Text assignments (fmImport_Load and the static ImportFiles entry). - UpdateSharedFolderAffordance takes an "announce" flag. The visual banner and cbPath.AccessibleDescription always update; only the one-time full-path announcement is gated. User-driven changes (typing/Browse/selection) still raise it at MessageOptions.All (dialog); initial load / programmatic set uses Both (status bar + log), so no modal appears out of context. Also reword and de-escalate the mask-skip notice (from an earlier fix in this file): a mask-based importer that finds files in the primary folder no longer raises a modal "NOT importing ..." dialog. It now reads "Files matching '*.BLG' were imported from the primary folder. Additional matching files in <shared> were not also imported (to avoid re-processing) ..." at MessageOptions.Both. This is safe because the sibling is only skipped when the primary already imported those files, so the data is always imported from somewhere. Build clean; 298 unit tests pass.
… no files" An enabled importer that finds no matching files is informational (nothing to import), not a warning or error, but it was surfaced with MessageOptions.All - a modal OK dialog. Opening import with several importers enabled could chain multiple such dialogs, which only annoys the user. Downgrade both "enabled but found NO matching files" messages from All to Both (status bar + log): they stay fully recorded and visible, without a dialog. - Per-importer message in EnumFiles (e.g. Trace Event Importer .xel masks). - The equivalent Custom XEL "no matching files" message. The /M automation exit-code logic (enabledButEmptyImporters / RequestedImporterMissingOrEmpty / enabledByToken) is unchanged - only the message severity changed - so requested-data-missing still returns a non-zero exit code. Genuine warnings/errors (missing primary folder, enumeration failures, different-size duplicates) keep their higher visibility. Build clean; 298 unit tests pass.
.....
Reviewer follow-up - what changed today, and where we divergedThanks for the thorough pass. Addressed across 7 commits; build clean, 298 tests DONEBugs / resilience
High-DPI / layout (your item 1 + 10-RED)
Logging visibility / noise
Accessibility
Error handling
Comments / docs / tests
Modal-noise reduction (extends your theme of dialogs-only-for-actionable-things)
WHERE WE DIVERGED (please weigh in)
PARTIAL / MY CALL
CONFIRMS (outside the code)
|
Review — PR #558: Import sibling
|
Downgrade the sibling shared-folder announcement in fmImport from MessageOptions.All to MessageOptions.Both so it no longer raises a MessageBox on a user-driven path change. tbPath_TextChanged fires on every keystroke, so the modal could pop mid-entry, steal focus, and cause truncated/mistyped paths - a problem for keyboard and screen-reader users. The banner, tooltip, and cbPath.AccessibleDescription already convey the same information. - Change MessageOptions.All to MessageOptions.Both in UpdateSharedFolderAffordance (status bar + log, no dialog). - Remove the now-unnecessary m_suppressAffordanceDialog field and its two try/finally blocks (ImportFiles and form load). - Drop the 'announce' parameter from UpdateSharedFolderAffordance. - Update comments to reflect the non-modal affordance. No behavior change for console/quiet runs (already suppressed via Globals.ConsoleMode). Build verified green.
…ruth The SQLDiag / AlwaysOn_health / system_health file masks were declared twice - as local variables in CustomXELImporter.Load*Files() and as a copy in fmImport.CustomXelMasks. A "kept in sync" comment was the only link, so editing a mask in the importer (or a LogScout output rename) would silently stop the SharedOutputFiles sibling-gap warning from firing, and users would lose Custom XEL data with no build error or failing test - reintroducing the exact silent-failure this PR prevents. - Promote the three masks to internal const on CustomXELImporter (SqlDiagMask, AlwaysOnHealthMask, SystemHealthMask) plus an aggregate CustomXelFileMasks array. - Load*Files() now use the shared constants for both Directory.GetFiles and their log messages. - fmImport.CustomXelMasks now references CustomXELImporter.CustomXelFileMasks instead of copying the literals. - Add regression tests pinning the mask contract so any future rename fails a test rather than silently desyncing the warning. Build verified green; all CustomXELImporter tests pass.
…csproj The comment claimed "a strong-named test assembly cannot load an unsigned reference at runtime (FileLoadException)." That is false on .NET Framework - a strong-named assembly can reference/load a weak-named one; the only effect is the CS8002 warning, which the test project already suppresses via NoWarn. Reword the comment to cite the actual reason signing is required here: a signed assembly can only grant InternalsVisibleTo to a strong-named friend, and the grant must carry the friend's PublicKey (see AssemblyInfo.cs). This prevents a future maintainer from inheriting a false constraint or over-strong-naming other assemblies to "fix" CS8002. Comment-only change; no code behavior or build impact. Signing itself is still required (McpServer exposes internals consumed by the unit tests).
We cannot rebuild .CHM file. Can you file a separate issue on this and we can address in the future to perhaps remove the .CHM file?
Already worked on this in previous iteration. I made the choice not to change this - documented in previous iteration.
Implemented - removed the Modal diaglog. Simpler and not needed reaally
Implemented - simplifies things
Do nothing to code; fix the doc is the option, but not going to bother at this point.
Fixed the comment
At this point, small things these Nits. We'll leave them for some other time. |
#556 - Import files from SQL LogScout's sibling "SharedOutputFiles" folder
WHAT & WHY
Adds support for SQL LogScout "All Instances" captures where non-instance-specific (host/OS) logs live in a sibling \SharedOutputFiles folder next to the pointed instance folder. When that sibling exists, SQL Nexus now searches both the primary folder and the sibling; when it does not exist, behavior is unchanged.
SQL LogScout's "All Instances" capture splits output: instance-specific files go in an instance subfolder (e.g. output\SERVER_SQL2019), while non-instance host/OS diagnostics (running drivers, disk info, event logs, some .blg/.perf) go in a sibling folder named "SharedOutputFiles". Previously SQL Nexus only imported the folder the user selected, so the host/OS data was silently missed.
This PR makes SQL Nexus also scan the sibling SharedOutputFiles folder (when it exists as a direct sibling of the selected folder), with guards so files present in both folders are never imported twice.
BEHAVIOR
are searched; if it does not exist, behavior is unchanged (single folder).
name matches one already selected from the primary folder is skipped (primary
wins). Same name + same size -> quiet skip; same name + DIFFERENT size -> one
aggregated warning (a copy was not imported).
FALLBACK - scanned only when the primary matched nothing, so table setup is not
re-run over already-imported data. If the primary matched, an informational
(non-modal) note explains the sibling copies were not also imported.
folder only; sibling-ONLY Custom XEL files are surfaced as a "move these" note
(they are never auto-imported because the load uses SELECT * INTO + DROP TABLE).
UI / ACCESSIBILITY
when a sibling exists; full path in tooltip, AccessibleName, and log. It updates
in lockstep with the path box and no longer pops a modal dialog on form open -
the loud one-time announcement fires only on a user-driven path change.
SIGNING (enabling test coverage)
To let the unit test project reach internal members without widening them to
public plugin API, sqlnexus.exe and SqlNexus.McpServer.exe are now strong-named
with the existing shared SqlNexus.snk, the test project is strong-named with the
same key, and the InternalsVisibleTo grants (sqlnexus, ErrorLogImporter,
SqlNexus.McpServer) use the key-qualified form. Plugin loading is by path
(Assembly.LoadFile), so it is unaffected; Authenticode release signing is
orthogonal and unaffected. Re-enabling the sqlnexus tests in the CLI runner also
surfaced a latent IsDbNameValid(null/"") crash, now fixed (fail closed).
RESILIENCE / ERRORS
IOException | PathTooLongException): a locked/too-long sibling is logged and the
scan continues on the primary folder.
sibling stays silent (normal).
TESTS
resolution (happy path, no-sibling, null/empty/whitespace, trailing separator,
quoted input, drive root, UNC, case-insensitive folder name, .. normalization),
duplicate classification (same/different/unknown size), sibling-only detection,
the mask-skip rule, and the row target-path / display-text composition helpers.
DOCS
"(from SharedOutputFiles)" suffix.
key-qualified InternalsVisibleTo requirement.
OUT OF SCOPE / HYGIENE