From 33b354b7797013db163531bea7fb73321111482b Mon Sep 17 00:00:00 2001 From: DonislawDev Date: Tue, 20 Oct 2026 12:31:48 +0200 Subject: [PATCH 1/4] Bump CsWin32 to 0.3.360 and call the pointer overloads it now generates CsWin32 0.3.358 keeps buffers that hold interior pointers as native pointers, so EnumServicesStatusEx and GetTokenInformation lost their span overloads. The listing already pinned its block across the call and the reading, so it now hands over that pointer and the length. The elevation type is read straight into a local of its own type. Supersedes the Dependabot pull request for the same version, which did not build. Co-Authored-By: Claude Opus 5.5 --- src/Bws.Core/Bws.Core.csproj | 2 +- src/Bws.Core/Session.cs | 17 +++++++++++------ src/Bws.Core/WindowsScmCatalog.Enumerating.cs | 9 ++++++--- 3 files changed, 18 insertions(+), 10 deletions(-) diff --git a/src/Bws.Core/Bws.Core.csproj b/src/Bws.Core/Bws.Core.csproj index aeeb9ec2..46b39ede 100644 --- a/src/Bws.Core/Bws.Core.csproj +++ b/src/Bws.Core/Bws.Core.csproj @@ -17,7 +17,7 @@ build time and leaves nothing behind at runtime, which is why ADR-15 accepts it while the project otherwise keeps its dependency list short. --> - + runtime; build; native; contentfiles; analyzers; buildtransitive all diff --git a/src/Bws.Core/Session.cs b/src/Bws.Core/Session.cs index 0e7dcaa7..43926934 100644 --- a/src/Bws.Core/Session.cs +++ b/src/Bws.Core/Session.cs @@ -1,4 +1,3 @@ -using System.Buffers.Binary; using System.Security.Principal; using Windows.Win32; using Windows.Win32.Security; @@ -71,18 +70,24 @@ internal static bool ElevatedWithTwin(bool elevated, TOKEN_ELEVATION_TYPE? type) /// /// The elevation type of this process's token, or nothing when Windows would not say. /// - internal static TOKEN_ELEVATION_TYPE? ElevationType() + internal static unsafe TOKEN_ELEVATION_TYPE? ElevationType() { using var identity = WindowsIdentity.GetCurrent(); - Span answer = stackalloc byte[sizeof(int)]; - if (!PInvoke.GetTokenInformation(identity.AccessToken, TOKEN_INFORMATION_CLASS.TokenElevationType, answer, out var written) - || written != sizeof(int)) + // Windows writes the answer straight into a local of the type it answers with. CsWin32 + // 0.3.355 generated a span overload for this call, 0.3.360 generates only the pointer and + // the length, and a local on the stack does not move, so nothing needs pinning. + TOKEN_ELEVATION_TYPE answer; + + if (!PInvoke.GetTokenInformation( + identity.AccessToken, TOKEN_INFORMATION_CLASS.TokenElevationType, + &answer, (uint)sizeof(TOKEN_ELEVATION_TYPE), out var written) + || written != sizeof(TOKEN_ELEVATION_TYPE)) { return null; } - return (TOKEN_ELEVATION_TYPE)BinaryPrimitives.ReadInt32LittleEndian(answer); + return answer; } /// diff --git a/src/Bws.Core/WindowsScmCatalog.Enumerating.cs b/src/Bws.Core/WindowsScmCatalog.Enumerating.cs index 92f93394..fe7b2378 100644 --- a/src/Bws.Core/WindowsScmCatalog.Enumerating.cs +++ b/src/Bws.Core/WindowsScmCatalog.Enumerating.cs @@ -27,7 +27,7 @@ public sealed partial class WindowsScmCatalog // A list rather than a sequence, because the caller needs a count before it starts and an // index while it runs. It was already building one internally - the sequence was hiding // that behind a type that promised less than it delivered. - private static List Enumerate(SafeHandle manager) + private static unsafe List Enumerate(SafeHandle manager) { uint resume = 0; var stalled = 0; @@ -44,7 +44,7 @@ private static List Enumerate(SafeHandle manager) // how much room it wants, which is the documented way to size this. var probed = PInvoke.EnumServicesStatusEx( manager, SC_ENUM_TYPE.SC_ENUM_PROCESS_INFO, AllEntryTypes, - ENUM_SERVICE_STATE.SERVICE_STATE_ALL, default, + ENUM_SERVICE_STATE.SERVICE_STATE_ALL, null, 0, out var needed, out _, ref resume, null!); var probeError = Marshal.GetLastWin32Error(); @@ -189,11 +189,14 @@ private static unsafe uint ReadTurn(SafeHandle manager, uint needed, ref uint re // Pinned across the call and the reading, for the reason set out in full at // ReadConfiguration: each record here carries two absolute pointers into this very // block, and the array is free to move the instant the interop wrapper returns. + // CsWin32 0.3.360 stopped offering a span here for that same reason - its release + // notes for 0.3.358 keep buffers holding interior pointers as native pointers - so + // the pointer this block was already pinned for is now what the call takes. fixed (byte* pinned = buffer) { var read = PInvoke.EnumServicesStatusEx( manager, SC_ENUM_TYPE.SC_ENUM_PROCESS_INFO, AllEntryTypes, - ENUM_SERVICE_STATE.SERVICE_STATE_ALL, new Span(pinned, buffer.Length), + ENUM_SERVICE_STATE.SERVICE_STATE_ALL, pinned, (uint)buffer.Length, out _, out var returned, ref resume, null!); var error = Marshal.GetLastWin32Error(); From 5045377edeeece8a90d26d4fc07426ac9ccf00f9 Mon Sep 17 00:00:00 2001 From: DonislawDev Date: Tue, 20 Oct 2026 13:55:17 +0200 Subject: [PATCH 2/4] Show the change history in the window "Change history" under the bar's audit button shows, in place of the list, who changed a start type and who installed a service, read from the System log the way bws history reads it, with the same sentences about how far back the log reaches. Changes identical apart from their time share one line with a count and the first and last time, and "Show" puts each change on its own line or narrows the table to what SYSTEM did not do, to installations or to start type changes. The details panel ends with the entry's own history. The log is read when first asked for, again on F5 and on each opening, on one background thread, one reading at a time. A failed reading is a sentence with the way out and leaves nothing of an earlier one. F5 keeps the table's sort and both ticks. "Snapshots..." becomes "Audit..." with four items, because a fifth button does not fit the bar at the narrowest window. The comparison's "Show" row becomes ShowRow over IRowChoices, with a separator between groups of choices, its rows become ShownRows, and both read-only tables share a ReadOnlyTable style. The core gains ChangeGroup, which no output carries. Co-Authored-By: Claude Opus 5.5 --- CHANGELOG.md | 12 +- README.md | 41 ++- site/pages/faq/en.html | 4 +- site/pages/faq/pl.html | 4 +- site/pages/home/en.html | 2 +- site/pages/home/pl.html | 2 +- site/pages/snapshots-and-drift/en.html | 2 +- site/pages/snapshots-and-drift/pl.html | 2 +- src/Bws.Core/History/ChangeGroup.cs | 112 ++++++ src/Bws.Gui/ActionBar.xaml | 25 +- src/Bws.Gui/ActionBar.xaml.cs | 32 +- src/Bws.Gui/DiffFilterRow.xaml.cs | 43 --- src/Bws.Gui/DiffTable.xaml | 18 +- src/Bws.Gui/DiffView.xaml | 8 +- src/Bws.Gui/HistoryDoors.cs | 33 ++ src/Bws.Gui/HistoryTable.xaml | 82 +++++ src/Bws.Gui/HistoryTable.xaml.cs | 12 + src/Bws.Gui/HistoryView.xaml | 86 +++++ src/Bws.Gui/HistoryView.xaml.cs | 24 ++ src/Bws.Gui/MainWindow.Overview.cs | 10 +- src/Bws.Gui/MainWindow.xaml | 7 + src/Bws.Gui/MainWindow.xaml.cs | 9 +- src/Bws.Gui/Resources/gui.en.json | 56 ++- .../{DiffFilterRow.xaml => ShowRow.xaml} | 32 +- src/Bws.Gui/ShowRow.xaml.cs | 96 +++++ src/Bws.Gui/SnapshotDoors.cs | 4 + src/Bws.Gui/Themes/Cells.xaml | 30 ++ src/Bws.Gui/Themes/Menus.xaml | 11 +- .../ViewModels/Catalogue.Comparisons.cs | 13 +- src/Bws.Gui/ViewModels/Catalogue.Histories.cs | 125 +++++++ src/Bws.Gui/ViewModels/Catalogue.Views.cs | 8 +- src/Bws.Gui/ViewModels/Chosen.cs | 40 ++- src/Bws.Gui/ViewModels/Details.cs | 21 +- src/Bws.Gui/ViewModels/DiffFilter.cs | 132 ++----- src/Bws.Gui/ViewModels/HistoryFilter.cs | 227 ++++++++++++ src/Bws.Gui/ViewModels/HistoryReading.cs | 231 ++++++++++++ src/Bws.Gui/ViewModels/HistoryRow.cs | 57 +++ src/Bws.Gui/ViewModels/HistorySheet.cs | 77 ++++ src/Bws.Gui/ViewModels/HistoryWords.cs | 153 ++++++++ .../ViewModels/MainViewModel.History.cs | 50 +++ .../ViewModels/MainViewModel.Snapshots.cs | 12 +- src/Bws.Gui/ViewModels/MainViewModel.cs | 20 +- src/Bws.Gui/ViewModels/RowChoices.cs | 130 +++++++ .../ConcurrencyGuards.cs | 10 + tests/Bws.Core.Tests/ChangeGroupTests.cs | 177 +++++++++ tests/Bws.Gui.Tests/CountedWords.cs | 6 +- tests/Bws.Gui.Tests/DiffFilterTests.cs | 2 +- tests/Bws.Gui.Tests/HistoryLog.cs | 94 +++++ tests/Bws.Gui.Tests/HistoryTests.cs | 339 ++++++++++++++++++ tests/Bws.Gui.Tests/HistoryWindowGuards.cs | 99 +++++ tests/Bws.Gui.Tests/SnapshotWindowGuards.cs | 14 +- .../Bws.Gui.Tests/TwoSnapshotWindowGuards.cs | 2 +- tests/mutation/entries.ps1 | 185 ++++++++-- 53 files changed, 2735 insertions(+), 288 deletions(-) create mode 100644 src/Bws.Core/History/ChangeGroup.cs delete mode 100644 src/Bws.Gui/DiffFilterRow.xaml.cs create mode 100644 src/Bws.Gui/HistoryDoors.cs create mode 100644 src/Bws.Gui/HistoryTable.xaml create mode 100644 src/Bws.Gui/HistoryTable.xaml.cs create mode 100644 src/Bws.Gui/HistoryView.xaml create mode 100644 src/Bws.Gui/HistoryView.xaml.cs rename src/Bws.Gui/{DiffFilterRow.xaml => ShowRow.xaml} (55%) create mode 100644 src/Bws.Gui/ShowRow.xaml.cs create mode 100644 src/Bws.Gui/ViewModels/Catalogue.Histories.cs create mode 100644 src/Bws.Gui/ViewModels/HistoryFilter.cs create mode 100644 src/Bws.Gui/ViewModels/HistoryReading.cs create mode 100644 src/Bws.Gui/ViewModels/HistoryRow.cs create mode 100644 src/Bws.Gui/ViewModels/HistorySheet.cs create mode 100644 src/Bws.Gui/ViewModels/HistoryWords.cs create mode 100644 src/Bws.Gui/ViewModels/MainViewModel.History.cs create mode 100644 src/Bws.Gui/ViewModels/RowChoices.cs create mode 100644 tests/Bws.Core.Tests/ChangeGroupTests.cs create mode 100644 tests/Bws.Gui.Tests/HistoryLog.cs create mode 100644 tests/Bws.Gui.Tests/HistoryTests.cs create mode 100644 tests/Bws.Gui.Tests/HistoryWindowGuards.cs diff --git a/CHANGELOG.md b/CHANGELOG.md index e2b0b3f4..ce42c048 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -31,7 +31,7 @@ is not part of this repository. running `bws` before you upgrade, because a copy left running can stop with an error. Uninstalling removes the programs, the Start menu entry and the PATH entry, and leaves the window's own file in `%APPDATA%` and the folder it unpacks into as administrator under `ProgramData` alone. -- Snapshots in the window. A "Snapshots..." button beside Export... offers "Take snapshot...", which +- Snapshots in the window. An "Audit..." button beside Export... offers "Take snapshot...", which writes this machine to a file you choose - the same file `bws snapshot create` writes, readable by either - and "Compare with this machine...", which reads a snapshot and shows what changed since it in place of the list: one row per field that differs, the entries added and removed, and above @@ -50,6 +50,16 @@ is not part of this repository. "Take snapshot..." now opens a short page first: it says whether the window reads the machine with administrator rights, and takes a note that is kept inside the snapshot and shown whenever it is compared, as `bws snapshot create --note` does. +- The change history in the window. "Change history" under "Audit..." shows, in place of the list, + who changed the start type of a service and who installed one, from the System log - what + `bws history` reads - with the same sentences above it about how far back the log reaches and + whether it was cleared. Changes that are identical apart from their time share one line, which + says how many times and since when, so Windows switching a service back and forth by itself takes + two lines rather than a page. "Show" above the table puts each change on a line of its own, or + narrows it to the changes SYSTEM did not make, to installations or to start type changes, and says + how much is on screen. The panel about one entry ends with a "History" section: that entry's start + type changes, gathered the same way. The log is read the first time you ask, again on F5, and again + each time the history is opened. "Back to the list" or Escape returns. - A "GitHub" button left of Donate at the right end of the row above the search box. It opens the project's repository - the source, the issues and the releases - in your browser, the way Donate opens its page, and in a window running as administrator it asks the desktop to open it, so the diff --git a/README.md b/README.md index ae60c72e..3f40108b 100644 --- a/README.md +++ b/README.md @@ -135,8 +135,7 @@ nothing to download - what it carries, which version, under which licence. > **Early release.** Both halves do everything on this page, and an automated suite runs on every > commit. What is not there yet is under [Honest limits](#honest-limits) - most of it is the second -> half of the audit story: a stock Windows baseline, who changed what in the window, and restoring -> from a snapshot. +> half of the audit story: a stock Windows baseline and restoring from a snapshot. **One promise to hold it to:** nothing changes without showing the plan first, and the preview is what runs. A preview that differs from what then happened is the worst bug this tool can have - @@ -310,6 +309,13 @@ its SID otherwise - naming an account of a domain would mean asking a domain con tool does not go on the network. Start types come in the words Windows used, in the language of the machine. +In the window, *Audit...* and then *Change history* shows the same history in place of the list. +Changes that are identical apart from their time share one line, which says how many times and +since when - Windows switching a service back and forth by itself is two lines rather than over a +hundred, and nothing is left out. *Show* above the table puts each change on a line of its own, +or narrows it to what SYSTEM did not do, to installations, or to start type changes, and says how +much of the history is on screen. The panel about one entry ends with that entry's history. + --- ## Nothing happens without a plan @@ -408,11 +414,11 @@ A tool that quietly fails to cover something is worse than one that says what it - **No stock Windows baseline.** The opening screen has a place for *how many of these are not from a clean install* and says it is not counted yet rather than showing a number it cannot stand behind. -- **A thin change history, and only on the command line.** `bws history` reads who changed a start - type and who installed a service out of the System log, as far back as that log reaches. Windows - writes nothing there about a changed account, launch path or recovery list, or about who stopped - a service, so none of that is in it - a snapshot and a diff answer *what* changed for those. The - window does not show the history yet. +- **A thin change history.** `bws history` and the window's *Change history* read who changed a + start type and who installed a service out of the System log, as far back as that log reaches. + Windows writes nothing there about a changed account, launch path or recovery list, or about who + stopped a service, so none of that is in it - a snapshot and a diff answer *what* changed for + those. - **No restore from a snapshot.** A diff tells you what drifted, and putting it back is by hand or by script from the commands the diff shows you. - **No creating, deleting or repointing a service.** The launch path is read and reported, and it @@ -627,12 +633,13 @@ by another program, which it then says, leaves alone and saves nothing over. A r every operation before offering it, and copies the name or everything about the entry, display name and description included. *Export...* writes the rows on screen, in the columns you have on and the order you sorted them into, to a CSV file separated the way your Windows region -settings separate lists, so a double click opens it in columns. *Snapshots...* beside it takes a +settings separate lists, so a double click opens it in columns. *Audit...* beside it takes a snapshot of the machine to a file, the same file `bws snapshot create` writes, with a note kept inside it if you type one, and compares one with the machine as it is now, or two with each other, showing what changed in place of the list, one row per field. *Show* above that table narrows it - to the differences alone, say, or to one field - and *Export...* there writes the comparison to a -CSV file, what the window says above the table first. Ctrl+C +CSV file, what the window says above the table first. *Change history* under the same button shows +who changed a start type or installed a service, from the System log. Ctrl+C over the list copies everything about the chosen entry. Escape backs out of the innermost thing first. *GitHub* and *Donate*, at the right end of the row above the search box, open the project's repository and its support page in @@ -686,11 +693,11 @@ endorsed by or sponsored by Microsoft. Windows is a trademark of the Microsoft g ### What about the second half - who changed it, and putting it back? -Half of the first half. `bws history` reads who changed a start type and who installed a service -from the System log, on the command line - see [And who changed it](#and-who-changed-it). The same -history in the window, a stock Windows baseline per build, and restoring configuration from a -snapshot with a dry run and a checkbox per item are the plan. A snapshot at deployment and a diff -against the machine answer *what* changed for everything the log does not record. +Half of the first half. `bws history` and the window's *Change history* read who changed a start +type and who installed a service from the System log - see [And who changed it](#and-who-changed-it). +A stock Windows baseline per build, and restoring configuration from a snapshot with a dry run and a +checkbox per item, are the plan. A snapshot at deployment and a diff against the machine answer +*what* changed for everything the log does not record. --- @@ -700,12 +707,12 @@ against the machine answer *what* changed for everything the log does not record plans with preview, cascade, refusal and the way back, stop, start, restart, force stop, force restart and start type, snapshots and diffs between files and against the live machine - in the window too, against the live machine - who changed a start type or installed a service, from the -System log on the command line, signatures, +System log, in the window and on the command line, signatures, privileges, security descriptors, triggers, memory, per-user folding, keyboard and screen reader in the window, CSV export, JSON and exit codes on the command line. -**Not there yet:** everything under [Honest limits](#honest-limits), above all the stock baseline, -the change history in the window and restore from a snapshot. +**Not there yet:** everything under [Honest limits](#honest-limits), above all the stock baseline +and restore from a snapshot. Found a problem, or a service this tool reports differently from `sc.exe`? The [issue tracker](https://github.com/donislawdev/BetterWindowsServices/issues) is open, and a diff diff --git a/site/pages/faq/en.html b/site/pages/faq/en.html index 572c24c7..a3a87792 100644 --- a/site/pages/faq/en.html +++ b/site/pages/faq/en.html @@ -43,7 +43,7 @@

Why is the window dark when my Windows is light?

What about the second half - who changed it, and putting it back?

-

Half of the first half. bws history reads who changed a start type and who installed a service out of the System log, on the command line. The same history in the window, a stock Windows baseline per build, and restoring configuration from a snapshot with a dry run and a checkbox per item are the plan. A snapshot at deployment and a diff against the machine answer what changed for everything the log does not record.

+

Half of the first half. bws history and the window's Change history read who changed a start type and who installed a service out of the System log. A stock Windows baseline per build, and restoring configuration from a snapshot with a dry run and a checkbox per item, are the plan. A snapshot at deployment and a diff against the machine answer what changed for everything the log does not record.

@@ -57,7 +57,7 @@

Not built yet

  • Snapshots are on the command line only. The window shows, searches and changes, and it does not take or compare snapshots. Use bws snapshot beside it.
  • No stock Windows baseline. The opening screen has a place for how many of these are not from a clean install and says it is not counted yet rather than showing a number it cannot stand behind.
  • -
  • A thin change history, and only on the command line. bws history reads who changed a start type and who installed a service, as far back as the System log reaches. Windows writes nothing there about a changed account, launch path or recovery list, or about who stopped a service.
  • +
  • A thin change history. bws history and the window's Change history read who changed a start type and who installed a service, as far back as the System log reaches. Windows writes nothing there about a changed account, launch path or recovery list, or about who stopped a service.
  • No restore from a snapshot. A diff tells you what drifted, and putting it back is by hand or by script from the commands the diff shows you.
  • No creating, deleting or repointing a service. The launch path is read and reported, and it is not edited.
  • No recovery actions. Not read, not shown, not searchable.
  • diff --git a/site/pages/faq/pl.html b/site/pages/faq/pl.html index e5f8a491..bf71bc9c 100644 --- a/site/pages/faq/pl.html +++ b/site/pages/faq/pl.html @@ -47,7 +47,7 @@

    Dlaczego okno jest ciemne, gdy mój Windows jest jasny?

    A druga połowa - kto to zmienił i jak to cofnąć?

    -

    Połowa pierwszej połowy. bws history czyta z dziennika System, kto zmienił typ uruchomienia i kto zainstalował usługę - w wierszu poleceń. Ta sama historia w oknie, wzorzec czystego Windowsa na wydanie i przywracanie konfiguracji ze snapshotu z podglądem i polem wyboru przy każdej pozycji - to jest plan. Snapshot przy wdrożeniu i różnica wobec maszyny odpowiadają na co się zmieniło przy wszystkim, czego dziennik nie zapisuje.

    +

    Połowa pierwszej połowy. bws history i Change history w oknie czytają z dziennika System, kto zmienił typ uruchomienia i kto zainstalował usługę. Wzorzec czystego Windowsa na wydanie i przywracanie konfiguracji ze snapshotu z podglądem i polem wyboru przy każdej pozycji - to jest plan. Snapshot przy wdrożeniu i różnica wobec maszyny odpowiadają na co się zmieniło przy wszystkim, czego dziennik nie zapisuje.

    @@ -61,7 +61,7 @@

    Jeszcze nie zbudowane

    • Snapshoty są tylko w wierszu poleceń. Okno pokazuje, szuka i zmienia, a snapshotów nie robi ani nie porównuje. Do tego jest bws snapshot obok.
    • Brak wzorca czystego Windowsa. Ekran startowy ma miejsce na ile z nich nie pochodzi z czystej instalacji i mówi, że tego jeszcze nie liczy, zamiast pokazywać liczbę, za którą nie może ręczyć.
    • -
    • Cienki dziennik zmian, i tylko w wierszu poleceń. bws history czyta, kto zmienił typ uruchomienia i kto zainstalował usługę, tak daleko wstecz, jak sięga dziennik System. O zmienionym koncie, ścieżce, akcjach naprawczych ani o tym, kto zatrzymał usługę, Windows nic tam nie zapisuje.
    • +
    • Cienki dziennik zmian. bws history i Change history w oknie czytają, kto zmienił typ uruchomienia i kto zainstalował usługę, tak daleko wstecz, jak sięga dziennik System. O zmienionym koncie, ścieżce, akcjach naprawczych ani o tym, kto zatrzymał usługę, Windows nic tam nie zapisuje.
    • Brak przywracania ze snapshotu. Różnica mówi, co się rozjechało, a przywrócenie robi się ręcznie albo skryptem z komend, które ta różnica pokazuje.
    • Brak tworzenia, usuwania i przestawiania usługi. Ścieżka uruchomienia jest czytana i pokazywana, i nie jest edytowana.
    • Brak akcji odzyskiwania. Nieczytane, niepokazywane, nieprzeszukiwalne.
    • diff --git a/site/pages/home/en.html b/site/pages/home/en.html index bc976794..e7ded5a2 100644 --- a/site/pages/home/en.html +++ b/site/pages/home/en.html @@ -180,7 +180,7 @@

      What is not there yet

      Snapshots are on the command line only

      The window shows, searches and changes, and it does not take or compare snapshots. Use bws snapshot beside it.

      No stock Windows baseline

      The opening screen has a place for how many of these are not from a clean install and says it is not counted yet rather than showing a number it cannot stand behind.

      -

      A thin change history, no restore

      Who changed a start type or installed a service comes from the System log on the command line, and only as far back as that log reaches. A diff tells you what drifted, and putting it back is by hand or by script from the commands the diff shows you.

      +

      A thin change history, no restore

      Who changed a start type or installed a service comes from the System log, in the window and on the command line, and only as far back as that log reaches. A diff tells you what drifted, and putting it back is by hand or by script from the commands the diff shows you.

      Local machine only

      Remote management is not in this version. To compare two machines, take a snapshot on each and diff the two files. Windows only, 64-bit only.

      The rest of the limits, and the questions people ask diff --git a/site/pages/home/pl.html b/site/pages/home/pl.html index fefee833..dc88c48f 100644 --- a/site/pages/home/pl.html +++ b/site/pages/home/pl.html @@ -166,7 +166,7 @@

      Czego jeszcze nie ma

      Snapshoty tylko w wierszu poleceń

      Okno pokazuje, szuka i zmienia, a snapshotów nie robi ani nie porównuje. Do tego jest bws snapshot obok.

      Brak wzorca czystego Windowsa

      Ekran startowy ma miejsce na ile z nich nie pochodzi z czystej instalacji i mówi, że tego jeszcze nie liczy, zamiast pokazywać liczbę, za którą nie może ręczyć.

      -

      Cienki dziennik zmian, bez przywracania

      Kto zmienił typ uruchomienia albo zainstalował usługę - to przychodzi z dziennika System w wierszu poleceń i tylko tak daleko wstecz, jak sięga ten dziennik. Różnica mówi, co się rozjechało, a przywrócenie robi się ręcznie albo skryptem z komend, które ta różnica pokazuje.

      +

      Cienki dziennik zmian, bez przywracania

      Kto zmienił typ uruchomienia albo zainstalował usługę - to przychodzi z dziennika System, w oknie i w wierszu poleceń, i tylko tak daleko wstecz, jak sięga ten dziennik. Różnica mówi, co się rozjechało, a przywrócenie robi się ręcznie albo skryptem z komend, które ta różnica pokazuje.

      Tylko maszyna lokalna

      Zarządzania zdalnego w tej wersji nie ma. Żeby porównać dwie maszyny, zrób snapshot na każdej i porównaj pliki. Tylko Windows, tylko 64 bity.

      Reszta ograniczeń i pytania, które ludzie zadają diff --git a/site/pages/snapshots-and-drift/en.html b/site/pages/snapshots-and-drift/en.html index 0542a91d..f5881259 100644 --- a/site/pages/snapshots-and-drift/en.html +++ b/site/pages/snapshots-and-drift/en.html @@ -67,7 +67,7 @@

      What this does not do yet

      The second half of the audit story is not finished, and saying so is cheaper than somebody discovering it.

      • Snapshots are on the command line only. The window shows, searches and changes, and it does not take or compare them.
      • -
      • A thin change history. bws history reads who changed a start type and who installed a service out of the System log, on the command line and as far back as that log reaches. For everything else a snapshot at deployment and a diff against the machine answer what changed, which is most of the question.
      • +
      • A thin change history. bws history and the window's Change history read who changed a start type and who installed a service out of the System log, as far back as that log reaches. For everything else a snapshot at deployment and a diff against the machine answer what changed, which is most of the question.
      • No stock Windows baseline. How many of these are not from a clean install is not counted yet, and the opening screen says so rather than showing a number it cannot stand behind.
      • No restore. A diff tells you what drifted, and putting it back is by hand or by script from the commands the diff shows you.
      diff --git a/site/pages/snapshots-and-drift/pl.html b/site/pages/snapshots-and-drift/pl.html index 995368fb..38f5ec88 100644 --- a/site/pages/snapshots-and-drift/pl.html +++ b/site/pages/snapshots-and-drift/pl.html @@ -67,7 +67,7 @@

      Czego to jeszcze nie robi

      Druga połowa historii o audycie nie jest skończona, a powiedzenie tego jest tańsze niż odkrycie tego przez kogoś samodzielnie.

      • Snapshoty są tylko w wierszu poleceń. Okno pokazuje, szuka i zmienia, a snapshotów nie robi ani nie porównuje.
      • -
      • Cienki dziennik zmian. bws history czyta z dziennika System, kto zmienił typ uruchomienia i kto zainstalował usługę - w wierszu poleceń i tak daleko wstecz, jak sięga ten dziennik. Przy całej reszcie snapshot przy wdrożeniu i różnica wobec maszyny odpowiadają na co się zmieniło, czyli na większość pytania.
      • +
      • Cienki dziennik zmian. bws history i Change history w oknie czytają z dziennika System, kto zmienił typ uruchomienia i kto zainstalował usługę - tak daleko wstecz, jak sięga ten dziennik. Przy całej reszcie snapshot przy wdrożeniu i różnica wobec maszyny odpowiadają na co się zmieniło, czyli na większość pytania.
      • Nie ma wzorca czystego Windowsa. Które z nich nie pochodzą z czystej instalacji nie jest liczone, więc punktem odniesienia musi być snapshot, który sam zrobiłeś.
      • Nie ma przywracania. Różnica mówi, co się rozjechało, a przywrócenie robi się ręcznie albo skryptem z komend, które ta różnica pokazuje.
      diff --git a/src/Bws.Core/History/ChangeGroup.cs b/src/Bws.Core/History/ChangeGroup.cs new file mode 100644 index 00000000..beb44b80 --- /dev/null +++ b/src/Bws.Core/History/ChangeGroup.cs @@ -0,0 +1,112 @@ +namespace Bws.Core.History; + +/// +/// Changes the System log recorded more than once, identical in everything but their time - one +/// line where the log has many. +/// +/// WHY THIS EXISTS, MEASURED 2026-10-20 on the owner's machine (docs/PROJEKT-DRUGA-POLOWA-AUDYTU-20261014.md +/// section 10, F28 to F30): 328 changes in nine days, of which 262 were Windows switching BITS, +/// IsolationSession and TrustedInstaller between automatic and on demand by itself, and 56 were one +/// driver installed again every time a Python tool ran. Grouped, that is 21 lines, and nothing is left +/// out: each line says how many times and between when. Owner's decision J2-b, 2026-10-20. +/// +/// IDENTICAL MEANS EVERY FIELD BUT THE TIME, and two halves of that are decisions rather than +/// defaults. The internal name compares without regard to case, the way the service manager compares +/// names and the way ties a change to an entry. And the account +/// compares by SID alone, never by name - rule 3 of the project notes, the same reason +/// carries both. Everything else compares exactly, so a driver installed from two +/// different folders stays two lines, which is what an administrator would want to see. +/// +/// Upper case the invariant way stands for "without regard to case" here, and that is not +/// proven to be the same comparison makes for every string Windows could +/// write - NOT CHECKED against the names on any machine. Where the two rules would part, the cost is a +/// change on a line of its own that could have shared one, never a change lost. +/// +/// Not a contract. bws history lists every change separately and its JSON has no groups. +/// +/// +/// The member that came first in the order the changes were handed in - for a history, which lists +/// the newest first, the newest one. What the line shows besides the count. +/// +/// How many changes the group holds. One for a change recorded once. +/// The earliest time among them, or null when none of them carried a time. +/// The latest time among them, or null when none of them carried a time. +public sealed record ChangeGroup(ServiceChange Latest, int Times, DateTimeOffset? First, DateTimeOffset? Last) +{ + /// + /// The changes gathered into groups of identical ones, in the order their first member was handed + /// in - so a history listed newest first gives groups listed by their newest member. + /// + /// By position rather than by time, because a change may carry no time at all (see + /// ), and an order that had to decide where those go would be a second + /// opinion about the order the history already has. + /// + public static IReadOnlyList Of(IReadOnlyList changes) + { + ArgumentNullException.ThrowIfNull(changes); + + var members = new List>(); + var where = new Dictionary(); + + foreach (var change in changes) + { + var identity = Identity.Of(change); + + if (where.TryGetValue(identity, out var at)) + { + members[at].Add(change); + } + else + { + where[identity] = members.Count; + members.Add([change]); + } + } + + return [.. members.Select(Gathered)]; + } + + /// + /// Every change on its own, in the same shape - what the window shows when somebody asks for each + /// change separately, so the table has one kind of row whichever way it is read. + /// + public static IReadOnlyList Each(IReadOnlyList changes) + { + ArgumentNullException.ThrowIfNull(changes); + + return [.. changes.Select(change => new ChangeGroup(change, 1, change.At, change.At))]; + } + + private static ChangeGroup Gathered(List members) + { + var times = members.Where(member => member.At is not null).Select(member => member.At!.Value).ToList(); + + return new ChangeGroup( + members[0], + members.Count, + times.Count == 0 ? null : times.Min(), + times.Count == 0 ? null : times.Max()); + } + + /// + /// What two identical changes share: everything but the time - see the summary of + /// for the two fields that are not compared as written. + /// + /// A value the compiler compares, rather than a comparer written by hand - the record's own + /// equality takes every field, so a field added here is compared without anybody remembering to. + /// The internal name goes in upper case the invariant way, which is how an ordinal comparison that + /// ignores case decides, and the account goes in as its SID alone. + /// + private readonly record struct Identity( + ChangeKind Kind, string? ServiceName, string? DisplayName, string? Sid, string? BinaryPath, string? Account, WindowsWords Words) + { + internal static Identity Of(ServiceChange change) => new( + change.Kind, + change.ServiceName?.ToUpperInvariant(), + change.DisplayName, + change.By?.Sid, + change.BinaryPath, + change.Account, + change.Words); + } +} diff --git a/src/Bws.Gui/ActionBar.xaml b/src/Bws.Gui/ActionBar.xaml index a894dbc0..122f7ceb 100644 --- a/src/Bws.Gui/ActionBar.xaml +++ b/src/Bws.Gui/ActionBar.xaml @@ -165,24 +165,27 @@ Click="ExportAsked" /> - diff --git a/src/Bws.Gui/ActionBar.xaml.cs b/src/Bws.Gui/ActionBar.xaml.cs index 0123c2db..4ae55e73 100644 --- a/src/Bws.Gui/ActionBar.xaml.cs +++ b/src/Bws.Gui/ActionBar.xaml.cs @@ -34,16 +34,17 @@ public ActionBar() Picked(0, onlyDrivers: false); OfferTheSettings(); - OfferTheSnapshots(); + OfferTheAudit(); } /// - /// The three things under "Snapshots...", one item each, built from a list the way the startup - /// settings are - GUI rule 8, and the same reason: each item a real MenuItem with its own - /// automation peer, present before the menu has ever opened (`docs/10` traps 7 and 19). - /// "Compare two snapshots" joined this list on 2026-10-15 (W2). + /// The four things under "Audit...", one item each, built from a list the way the startup settings + /// are - GUI rule 8, and the same reason: each item a real MenuItem with its own automation peer, + /// present before the menu has ever opened (`docs/10` traps 7 and 19). "Compare two snapshots" joined + /// this list on 2026-10-15 (W2), "Change history" on 2026-10-20 (J2-a) - last, because the three + /// above it are one family and it is another. /// - private void OfferTheSnapshots() + private void OfferTheAudit() { var style = (Style)FindResource("RowMenuItem"); @@ -51,7 +52,8 @@ private void OfferTheSnapshots() [ (Texts.Of("gui.snapshot.take"), "snapshotTake", () => TakeSnapshotRequest?.Invoke(this, EventArgs.Empty)), (Texts.Of("gui.snapshot.compare"), "snapshotCompare", () => CompareRequest?.Invoke(this, EventArgs.Empty)), - (Texts.Of("gui.snapshot.compareTwo"), "snapshotCompareTwo", () => CompareTwoRequest?.Invoke(this, EventArgs.Empty)) + (Texts.Of("gui.snapshot.compareTwo"), "snapshotCompareTwo", () => CompareTwoRequest?.Invoke(this, EventArgs.Empty)), + (Texts.Of("gui.history.open"), "historyOpen", () => HistoryRequest?.Invoke(this, EventArgs.Empty)) ]; foreach (var (words, id, asked) in items) @@ -61,18 +63,19 @@ private void OfferTheSnapshots() AutomationProperties.SetAutomationId(item, id); item.Click += (_, _) => asked(); - SnapshotsMenu.Items.Add(item); + AuditMenu.Items.Add(item); } } /// /// Whether the machine is being read for a snapshot - the button is off while it is, and says - /// why on itself, so a second press cannot ask for a second reading of the same thing. + /// why on itself, so a second press cannot ask for a second reading of the same thing. The change + /// history waits that second out with the rest of the menu: one button, one state. /// internal void ReadingForASnapshot(bool busy) { - SnapshotsButton.IsEnabled = !busy; - SnapshotsButton.ToolTip = busy ? Texts.Of("gui.action.snapshots.busy") : Texts.Of("gui.action.snapshots.hint"); + AuditButton.IsEnabled = !busy; + AuditButton.ToolTip = busy ? Texts.Of("gui.action.audit.busy") : Texts.Of("gui.action.audit.hint"); } /// Somebody asked for a snapshot of this machine, written to a file. @@ -84,8 +87,11 @@ internal void ReadingForASnapshot(bool busy) /// Somebody asked to compare two snapshots with each other. internal event EventHandler? CompareTwoRequest; - /// Opens the two items under the button, the way the startup settings open - ButtonMenu says why a left click. - private void SnapshotsAsked(object sender, RoutedEventArgs e) => ButtonMenu.OpenUnder(SnapshotsButton); + /// Somebody asked who changed what on this machine. + internal event EventHandler? HistoryRequest; + + /// Opens the four items under the button, the way the startup settings open - ButtonMenu says why a left click. + private void AuditAsked(object sender, RoutedEventArgs e) => ButtonMenu.OpenUnder(AuditButton); /// /// The startup settings under the button, one item per setting, in the order the command line diff --git a/src/Bws.Gui/DiffFilterRow.xaml.cs b/src/Bws.Gui/DiffFilterRow.xaml.cs deleted file mode 100644 index 1897d2c7..00000000 --- a/src/Bws.Gui/DiffFilterRow.xaml.cs +++ /dev/null @@ -1,43 +0,0 @@ -using System.Windows; -using System.Windows.Controls; -using Bws.Gui.ViewModels; - -namespace Bws.Gui; - -/// -/// "Show" over a comparison - the markup says the rest. -/// -/// The menu is made here, one MenuItem per choice, the way the bar makes the items under -/// "Snapshots..." - each a real item with its own automation peer from the moment the comparison -/// arrives, rather than containers an ItemsSource makes only once the menu opens (`docs/10` traps 7 and -/// 19). What a choice does is the filter's, bound through the item's style. -/// -public partial class DiffFilterRow : UserControl -{ - public DiffFilterRow() - { - InitializeComponent(); - - DataContextChanged += (_, _) => OfferTheChoices(); - } - - private void ShowAsked(object sender, RoutedEventArgs e) => ButtonMenu.OpenUnder(ShowButton); - - /// The choices of the comparison now on screen, as items of the menu under the button. - private void OfferTheChoices() - { - ShowMenu.Items.Clear(); - - if (DataContext is not DiffSheet sheet) - { - return; - } - - var style = (Style)FindResource("DiffChoiceItem"); - - foreach (var choice in sheet.Filter.Choices) - { - ShowMenu.Items.Add(new MenuItem { DataContext = choice, Style = style }); - } - } -} diff --git a/src/Bws.Gui/DiffTable.xaml b/src/Bws.Gui/DiffTable.xaml index cded0409..17c80913 100644 --- a/src/Bws.Gui/DiffTable.xaml +++ b/src/Bws.Gui/DiffTable.xaml @@ -23,23 +23,9 @@ + ItemsSource="{Binding Filter.Rows}"> - + + + + + + + + + + + + + + + + + + + diff --git a/src/Bws.Gui/HistoryTable.xaml.cs b/src/Bws.Gui/HistoryTable.xaml.cs new file mode 100644 index 00000000..524c771a --- /dev/null +++ b/src/Bws.Gui/HistoryTable.xaml.cs @@ -0,0 +1,12 @@ +using System.Windows.Controls; + +namespace Bws.Gui; + +/// The table under the change history, or the sentence in its place - the markup says the rest. +public partial class HistoryTable : UserControl +{ + public HistoryTable() + { + InitializeComponent(); + } +} diff --git a/src/Bws.Gui/HistoryView.xaml b/src/Bws.Gui/HistoryView.xaml new file mode 100644 index 00000000..5b71ccef --- /dev/null +++ b/src/Bws.Gui/HistoryView.xaml @@ -0,0 +1,86 @@ + + + + + + + + + + + + + + + + + + + + +