Service: isolate the profile in ProgramData, out of all normal profile paths
Local checkpoint - NOT for public release. Ed: the service profile must never be reachable except through the Service menu. Also fixes a real bug: the service runs as SYSTEM, whose per-user data folder is NOT the interactive user's - so a profile saved in the user's profiles folder (or AppConfig, both per-user) was invisible to the service. It would have idled, never streaming. - New RemSound.Core.ServiceStore: the service profile + its settings (logging) live in a MACHINE-WIDE ProgramData\RemSound\service location - same absolute path for the user (config dialog) and SYSTEM (service). Moved ServiceProfileName/ServiceLoggingEnabled off AppConfig (per-user) onto this store. - ServiceSendHost.FromConfig + RemSoundService now read ServiceStore; ConfigureServiceProfile saves there (and migrates + deletes any profile left in the old user-folder location). - Because it's no longer in the user's profiles folder, it can't appear in the startup picker, File->Open, Recent profiles, or the password manager (all of which read the user ProfileStore); the reserved-title filter in ListProfileTitles stays as belt-and-braces. - Password button renamed "Set service profile password". - New self-test "Service profile isolation": store is under ProgramData, the reserved title is filtered from the listing, and it round-trips through the machine-wide store. Gate 27/27. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.8
parent
f0b35b2b8c
commit
4f5265d8b0
@@ -64,6 +64,7 @@ internal static class SelfTest
|
||||
RunStep(results, "Lifecycle churn (modes, sources, pan/EQ, send/receive)", LifecycleChurn);
|
||||
RunStep(results, "Service app-yield token", ServiceInteractivePresence);
|
||||
RunStep(results, "Service sender parity (crypto + Opus frame)", ServiceSenderParity);
|
||||
RunStep(results, "Service profile isolation (location + hidden from pickers)", ServiceProfileIsolation);
|
||||
RunStep(results, "Service send host (headless stream + yield)", ServiceSendHostStream);
|
||||
RunStep(results, "Service registration args", ServiceRegistrationArgs);
|
||||
RunStep(results, "Recording engine (all formats + source gate + mono)", RecordingEngine);
|
||||
@@ -678,6 +679,52 @@ internal static class SelfTest
|
||||
return "sc create args quoted correctly for a spaced path";
|
||||
}
|
||||
|
||||
/// <summary>The service profile is fully isolated from the normal profile machinery: it lives in a
|
||||
/// MACHINE-WIDE ProgramData location (readable by the SYSTEM service, outside the user's profiles
|
||||
/// folder), and the reserved title never shows up in the profile listing that backs the startup
|
||||
/// picker, File→Open, Recent profiles and the password manager. Also round-trips through the store.</summary>
|
||||
private static string? ServiceProfileIsolation()
|
||||
{
|
||||
// 1. The store lives under ProgramData, NOT the user's profiles folder.
|
||||
var programData = Environment.GetFolderPath(Environment.SpecialFolder.CommonApplicationData);
|
||||
Check(ServiceStore.Directory.StartsWith(programData, StringComparison.OrdinalIgnoreCase),
|
||||
$"the service profile must live under ProgramData (got {ServiceStore.Directory})");
|
||||
|
||||
// 2. The reserved title is filtered out of ListProfileTitles (the picker / recents / password
|
||||
// manager all read that), even if a stray file were present in the profiles folder.
|
||||
var temp = Path.Combine(Path.GetTempPath(), "remsound-svciso-" + Guid.NewGuid().ToString("N"));
|
||||
try
|
||||
{
|
||||
var store = new ProfileStore(temp);
|
||||
store.Save(new Profile { Title = "My normal profile" });
|
||||
store.Save(new Profile { Title = ProfileStore.ReservedServiceProfileTitle });
|
||||
var titles = store.ListProfileTitles();
|
||||
Check(titles.Contains("My normal profile"), "a normal profile must be listed");
|
||||
Check(!titles.Any(t => string.Equals(t, ProfileStore.ReservedServiceProfileTitle, StringComparison.OrdinalIgnoreCase)),
|
||||
"the service profile must NOT appear in the profile listing (picker / recents / password manager)");
|
||||
|
||||
// 3. Round-trip through the machine-wide store (redirected to a temp folder for the test).
|
||||
var saved = ServiceStore.TestDirectoryOverride;
|
||||
ServiceStore.TestDirectoryOverride = Path.Combine(temp, "service");
|
||||
try
|
||||
{
|
||||
Check(ServiceStore.LoadProfile() is null, "no service profile before one is saved");
|
||||
var p = new Profile { Title = ProfileStore.ReservedServiceProfileTitle, WasapiSendMode = "applications" };
|
||||
p.SelectedConnectedPeers.Add("10.0.0.5");
|
||||
ServiceStore.SaveProfile(p);
|
||||
ServiceStore.SaveLoggingEnabled(true);
|
||||
var back = ServiceStore.LoadProfile();
|
||||
Check(back is not null && back.WasapiSendMode == "applications" && back.SelectedConnectedPeers.Contains("10.0.0.5"),
|
||||
"the service profile must round-trip through the machine-wide store");
|
||||
Check(ServiceStore.LoadLoggingEnabled(), "service logging flag must round-trip");
|
||||
}
|
||||
finally { ServiceStore.TestDirectoryOverride = saved; }
|
||||
|
||||
return "under ProgramData; hidden from the picker/recents/password-manager; round-trips";
|
||||
}
|
||||
finally { try { Directory.Delete(temp, recursive: true); } catch { /* best-effort */ } }
|
||||
}
|
||||
|
||||
/// <summary>The service must configure the sender EXACTLY like the main app: derive both the audio key
|
||||
/// AND the fingerprint from the password (a missing fingerprint gets the encrypted stream rejected at
|
||||
/// the peer), and apply the send-rate-adjusted Opus frame (the "Small" rate halves it). Guards the
|
||||
|
||||
Reference in New Issue
Block a user