From 44ac1147bc287f39b0bad34eafe9cf98020deeed Mon Sep 17 00:00:00 2001 From: Mack Straight Date: Sat, 3 Oct 2026 10:45:48 -0400 Subject: [PATCH 1/2] test: record what Windows names an endpoint whose ID is reused Adds SarTest endpoint-names and an issue-15-endpoint-names scenario. It runs a host with an endpoint pair, renames it keeping the IDs, then gives it new IDs, and records which name Windows shows each time. It fails unless the new IDs show the new name. Layouts get an ID prefix separate from the name prefix for this. Co-Authored-By: Claude Opus 5.5 --- SarTest/SarTest.cpp | 111 ++++++++++++++++++++++++++++++++++ SarTest/common.cpp | 4 +- SarTest/common.h | 2 + tools/test/Invoke-SarTest.ps1 | 2 + tools/test/README.md | 4 ++ 5 files changed, 121 insertions(+), 2 deletions(-) diff --git a/SarTest/SarTest.cpp b/SarTest/SarTest.cpp index c8f784a..bb7fc2a 100644 --- a/SarTest/SarTest.cpp +++ b/SarTest/SarTest.cpp @@ -59,6 +59,9 @@ int usage() " Open SAR with no hardware interface configured, the way a DAW sees\n" " it before first-time setup; fail unless it offers a stereo input\n" " and output (issues #31, #54).\n" + " SarTest endpoint-names\n" + " Rename an endpoint keeping its ID, then give it a new ID, and report\n" + " the name Windows shows each time (issues #15, #66).\n" "\n" "Layout options: --endpoints N (playback/recording pairs, default 2)\n" " --channels C (per endpoint, default 2) --prefix \n" @@ -711,6 +714,112 @@ int cmdNoInterface(const Args& args) return rc; } +// Starts a host on the layout, records whether Windows shows the layout's +// endpoints (and, when stale is given, endpoints under those names instead) +// and stops it again. +bool showsEndpoints(const Args& args, const EndpointLayout& layout, + const EndpointLayout *stale, bool *staleShown) +{ + if (!prepareConfig(args, layout)) { + return false; + } + + AsioHost host(hostOptions(args, layout)); + + if (!host.load() || !host.open() || !host.start()) { + host.close(); + return false; + } + + FoundEndpoints found; + bool shown = findEndpoints(layout, args.getInt(L"wait", 30), &found); + + found.release(); + + if (stale) { + *staleShown = findEndpoints(*stale, 5, &found); + found.release(); + } + + host.close(); + + if (!waitForEndpointsGone(layout, args.getInt(L"wait-gone", 30))) { + logf("Endpoints lingered after stop"); + } + + return shown; +} + +// Issues #15 and #66: SarAsio's control panel gave a new endpoint the lowest +// unused "ep_N", so a deleted endpoint's ID went to the next one, and Windows +// kept showing the old endpoint's name for it. Windows keys what it stores +// about an endpoint (its name, the user's settings) on the ID. Checks that a +// fresh ID gets the new name, and reports what a reused ID shows. +int cmdEndpointNames(const Args& args) +{ + EndpointLayout before = EndpointLayout::fromArgs(args); + + before.pairs = 1; + before.prefix = L"SarTest Alpha"; + before.idPrefix = L"SarTest-names"; + + EndpointLayout renamed = before; + + renamed.prefix = L"SarTest Beta"; + + EndpointLayout fresh = renamed; + + fresh.idPrefix = L"SarTest-names-fresh"; + + logf("--- %s, IDs %s ---", narrow(before.prefix).c_str(), narrow(before.idPrefix).c_str()); + + bool firstShown = showsEndpoints(args, before, nullptr, nullptr); + + logf("--- renamed to %s, same IDs ---", narrow(renamed.prefix).c_str()); + + bool oldNameShown = false; + bool renamedShown = showsEndpoints(args, renamed, &before, &oldNameShown); + + logf("--- %s, new IDs %s ---", narrow(fresh.prefix).c_str(), narrow(fresh.idPrefix).c_str()); + + bool freshShown = showsEndpoints(args, fresh, nullptr, nullptr); + + logf("Reused IDs: Windows shows the %s name", renamedShown ? "new" : + oldNameShown ? "old" : "neither"); + + int rc = 0; + std::string failure; + + if (!firstShown) { + rc = 2; + failure = "endpoints never appeared"; + } else if (!freshShown) { + rc = 2; + failure = "endpoints with new IDs did not show their names"; + } + + if (rc != 0) { + logf("%s", failure.c_str()); + } + + JsonObject result; + + result.setString("command", "endpoint-names") + .setBool("passed", rc == 0) + .setInt("exitCode", rc) + .setBool("reusedIdShowsNewName", renamedShown) + .setBool("reusedIdShowsOldName", oldNameShown) + .setBool("newIdShowsNewName", freshShown); + + if (!failure.empty()) { + result.setString("failure", failure); + } + + writeResults(args, result.str()); + logf("%s", rc == 0 ? "PASSED" : "FAILED"); + return rc; +} + } // namespace int wmain(int argc, wchar_t **argv) @@ -748,6 +857,8 @@ int wmain(int argc, wchar_t **argv) rc = cmdControlPanel(args); } else if (args.command == L"no-interface") { rc = cmdNoInterface(args); + } else if (args.command == L"endpoint-names") { + rc = cmdEndpointNames(args); } else { rc = usage(); } diff --git a/SarTest/common.cpp b/SarTest/common.cpp index e4ca56c..8db42b7 100644 --- a/SarTest/common.cpp +++ b/SarTest/common.cpp @@ -201,12 +201,12 @@ std::wstring EndpointLayout::recordingName(int pair) const std::string EndpointLayout::playbackId(int pair) const { - return narrow(prefix) + "-out-" + std::to_string(pair + 1); + return narrow(idPrefix.empty() ? prefix : idPrefix) + "-out-" + std::to_string(pair + 1); } std::string EndpointLayout::recordingId(int pair) const { - return narrow(prefix) + "-in-" + std::to_string(pair + 1); + return narrow(idPrefix.empty() ? prefix : idPrefix) + "-in-" + std::to_string(pair + 1); } EndpointLayout EndpointLayout::fromArgs(const Args& args) diff --git a/SarTest/common.h b/SarTest/common.h index 2c161df..9c7fce7 100644 --- a/SarTest/common.h +++ b/SarTest/common.h @@ -66,6 +66,8 @@ struct EndpointLayout int pairs = 2; int channels = 2; std::wstring prefix = L"SarTest"; + // Prefix of the endpoint IDs; the name prefix when empty. + std::wstring idPrefix; std::wstring playbackName(int pair) const; std::wstring recordingName(int pair) const; diff --git a/tools/test/Invoke-SarTest.ps1 b/tools/test/Invoke-SarTest.ps1 index 7ba008d..20da33a 100644 --- a/tools/test/Invoke-SarTest.ps1 +++ b/tools/test/Invoke-SarTest.ps1 @@ -151,6 +151,8 @@ if ($Scenarios -contains 'issues') { @('control-panel', '--endpoints', 1) + $common) -Timeout 120 $summary.scenarios += Invoke-Scenario 'issue-54-no-interface' ( @('no-interface') + $common) -Timeout 120 + $summary.scenarios += Invoke-Scenario 'issue-15-endpoint-names' ( + @('endpoint-names') + $common) -Timeout 300 } if ($Scenarios -contains 'matrix') { diff --git a/tools/test/README.md b/tools/test/README.md index 06374d1..8bf996c 100644 --- a/tools/test/README.md +++ b/tools/test/README.md @@ -88,6 +88,9 @@ host, presses the hardware interface's Configure button and checks that the clock never had a second instance alive. It needs an interactive desktop. `SarTest.exe no-interface` (#31, #54) opens SAR with no hardware interface configured and checks that it still offers a stereo input and output. +`SarTest.exe endpoint-names` (#15, #66) renames an endpoint keeping its ID, +then gives it a new ID, and checks that the new ID shows the new name; the +results record what Windows shows for the reused ID. `Invoke-SarTest.ps1 -Scenarios` selects any of `issues`, `matrix`, `race` and `kill`. @@ -103,6 +106,7 @@ SarTest run [--iterations K] [--duration S] SarTest race [--cycles K] [--up S] [--down MS] [--openers T] [--hold MS] SarTest control-panel SarTest no-interface +SarTest endpoint-names ``` `install` creates the SAR software device node if needed, installs the From 3e913200060f73959fe3fd99b1c729f0e92d918f Mon Sep 17 00:00:00 2001 From: Mack Straight Date: Sat, 3 Oct 2026 10:46:06 -0400 Subject: [PATCH 2/2] SarAsio: never reuse an endpoint ID for a new endpoint The control panel gave a new endpoint the lowest unused "ep_N", so after deleting an endpoint the next one added got its ID. Windows keeps an endpoint's name and settings under that ID, so it kept showing the deleted endpoint's name for the new one (and the same after a reinstall, where numbering starts over). New endpoints now get an ID made from a fresh GUID; the counter remains as a fallback if CoCreateGuid fails. Existing endpoints keep their IDs. Fixes #15, fixes #66. Co-Authored-By: Claude Opus 5.5 --- SarAsio/configui.cpp | 20 ++++++++++++++++++-- 1 file changed, 18 insertions(+), 2 deletions(-) diff --git a/SarAsio/configui.cpp b/SarAsio/configui.cpp index 3511610..53e96de 100644 --- a/SarAsio/configui.cpp +++ b/SarAsio/configui.cpp @@ -236,9 +236,25 @@ void EndpointsPropertySheetPage::onAddEndpoint() { _epDialogConfig = EndpointConfig(); + // Windows keeps what it knows about an endpoint (the name it shows, the + // user's settings) under its ID, so an ID must never be handed to a + // different endpoint: a deleted endpoint's ID given to a new one made + // Windows keep showing the deleted endpoint's name. + GUID guid; + + if (SUCCEEDED(CoCreateGuid(&guid))) { + char id[40]; + + sprintf_s(id, "ep_%08lx%04hx%04hx%02x%02x%02x%02x%02x%02x%02x%02x", + guid.Data1, guid.Data2, guid.Data3, + guid.Data4[0], guid.Data4[1], guid.Data4[2], guid.Data4[3], + guid.Data4[4], guid.Data4[5], guid.Data4[6], guid.Data4[7]); + _epDialogConfig.id = id; + } + int counter = 1; - do { + while (_epDialogConfig.id.empty()) { std::ostringstream os; os << "ep_" << counter; @@ -248,7 +264,7 @@ void EndpointsPropertySheetPage::onAddEndpoint() } else { counter++; } - } while (_epDialogConfig.id.empty()); + } auto result = DialogBoxParam( gDllModule,