diff --git a/MANUAL.md b/MANUAL.md index bd99086..18e3108 100644 --- a/MANUAL.md +++ b/MANUAL.md @@ -1097,7 +1097,8 @@ Option| What it does `--help` or `-h`| Lists every option, the same as this section in short form. `--version`| Prints which version of RemSound is installed, for example “RemSound 3.9”. `--devices`| Lists every microphone and line-in, every speaker and headphone output, and every ASIO driver on the machine — each with its sample rate, channel count and the exact device id RemSound uses internally. This is the quickest way to confirm an interface is actually present and seen by Windows. -`--selftest`| Runs RemSound's built-in self-test and reports **PASS** or **FAIL**. It works through a list of named checks: a full audio round-trip on the machine on its own (capture → encode → send across the network layer to itself → receive → decode, for both quality settings), the audio encryption, the network packet format, saving and reloading settings and a profile, that a diagnostics report never leaks a password, and that the bundled sounds and manual are present. No sound is played out, so it is safe to run silently. Add `--seconds N` to make the audio part run for longer than the default. +`--selftest` +(or `--smoke-test`)| Runs RemSound's built-in self-test and reports **PASS** or **FAIL**. It works through a list of named checks: a full audio round-trip on the machine on its own (capture → encode → send across the network layer to itself → receive → decode, for both quality settings), the audio encryption, the network packet format, saving and reloading settings and a profile, that a diagnostics report never leaks a password, and that the bundled sounds and manual are present. No sound is played out, so it is safe to run silently. Add `--seconds N` to make the audio part run for longer than the default. `--diagnostics`| Writes a single plain-text report file holding the version, the operating system, the current settings, the list of profiles, the full device list, a check of the Windows microphone-privacy permission, and the tail of the most recent log. With no path it saves into the **user settings and logs** folder and prints where it put it; you can also give a path, for example `--diagnostics C:\Users\you\Desktop\report.txt`. This is the file to send when asking for help — it answers most questions in one go. ### Options that change a setting or control a running copy, then exit @@ -1116,6 +1117,7 @@ Option| What it does `--profile ""`| Starts straight into the named profile and skips the profile picker. Put the name in quotes if it contains a space, for example `--profile "Studio link"`. `--connect `| Starts and connects to a peer at that address. You can give just an address (`--connect 192.168.1.42`) or an address and port (`--connect 192.168.1.42:47830`); with no port it uses RemSound's normal port, 47830. If you don't also give a `--profile`, it starts on a fresh blank profile already pointed at that peer. `--minimized` or `--tray`| Starts minimized to the notification area, with no window popping up. Pair it with `--profile` or `--connect` so it has something to do without waiting at the picker. +`--config-dir `| Uses an explicit folder for this run's settings, profiles, logs and sounds, instead of the usual location. It lets you (or an automated test) run RemSound against a throwaway folder without touching your real settings. Works with any command — for example `--selftest --config-dir C:\Temp\rstest` or `--diagnostics --config-dir C:\Temp\rstest`. ### Examples diff --git a/readme.html b/readme.html index 5610f79..db3149f 100644 --- a/readme.html +++ b/readme.html @@ -1154,7 +1154,7 @@ Use whatever key combinations you prefer (for example Ctrl+Shift+Up / Ctrl+Shift --help or -hLists every option, the same as this section in short form. --versionPrints which version of RemSound is installed, for example “RemSound 3.9”. --devicesLists every microphone and line-in, every speaker and headphone output, and every ASIO driver on the machine — each with its sample rate, channel count and the exact device id RemSound uses internally. This is the quickest way to confirm an interface is actually present and seen by Windows. ---selftestRuns RemSound's built-in self-test and reports PASS or FAIL. It works through a list of named checks: a full audio round-trip on the machine on its own (capture → encode → send across the network layer to itself → receive → decode, for both quality settings), the audio encryption, the network packet format, saving and reloading settings and a profile, that a diagnostics report never leaks a password, and that the bundled sounds and manual are present. No sound is played out, so it is safe to run silently. Add --seconds N to make the audio part run for longer than the default. +--selftest
(or --smoke-test)Runs RemSound's built-in self-test and reports PASS or FAIL. It works through a list of named checks: a full audio round-trip on the machine on its own (capture → encode → send across the network layer to itself → receive → decode, for both quality settings), the audio encryption, the network packet format, saving and reloading settings and a profile, that a diagnostics report never leaks a password, and that the bundled sounds and manual are present. No sound is played out, so it is safe to run silently. Add --seconds N to make the audio part run for longer than the default. --diagnosticsWrites a single plain-text report file holding the version, the operating system, the current settings, the list of profiles, the full device list, a check of the Windows microphone-privacy permission, and the tail of the most recent log. With no path it saves into the user settings and logs folder and prints where it put it; you can also give a path, for example --diagnostics C:\Users\you\Desktop\report.txt. This is the file to send when asking for help — it answers most questions in one go. @@ -1172,6 +1172,7 @@ Use whatever key combinations you prefer (for example Ctrl+Shift+Up / Ctrl+Shift --profile "<name>"Starts straight into the named profile and skips the profile picker. Put the name in quotes if it contains a space, for example --profile "Studio link". --connect <ip>Starts and connects to a peer at that address. You can give just an address (--connect 192.168.1.42) or an address and port (--connect 192.168.1.42:47830); with no port it uses RemSound's normal port, 47830. If you don't also give a --profile, it starts on a fresh blank profile already pointed at that peer. --minimized or --trayStarts minimized to the notification area, with no window popping up. Pair it with --profile or --connect so it has something to do without waiting at the picker. +--config-dir <folder>Uses an explicit folder for this run's settings, profiles, logs and sounds, instead of the usual location. It lets you (or an automated test) run RemSound against a throwaway folder without touching your real settings. Works with any command — for example --selftest --config-dir C:\Temp\rstest or --diagnostics --config-dir C:\Temp\rstest.

Examples

diff --git a/run-tests.ps1 b/run-tests.ps1 index a0178b2..f7f879b 100644 --- a/run-tests.ps1 +++ b/run-tests.ps1 @@ -113,6 +113,42 @@ foreach ($line in ($st.Text -split "`r?`n")) { } if ($st.Code -eq 0) { Pass "self-test passed (exit 0)" } else { Fail "self-test failed (exit $($st.Code))" } +# ---- 5. COLD START + CLEAN CLOSE, against an isolated --config-dir so the real settings are never +# touched (smoke-test brief, safety rule 1 + baseline steps 3-4) ---- +Write-Host "`nCold start and clean close (isolated config):" -ForegroundColor Cyan +$already = @(Get-Process RemSound -ErrorAction SilentlyContinue) +if ($already.Count -gt 0) { + Write-Host " [SKIP] a RemSound instance is already running (machine-wide single-instance lock) - close it to run this check" -ForegroundColor Yellow +} +else { + $testCfg = Join-Path ([System.IO.Path]::GetTempPath()) ("rs-cfg-" + [guid]::NewGuid().ToString('N')) + $proc = $null + try { + $proc = Start-Process -FilePath $exe -ArgumentList @('--config-dir', $testCfg, '--connect', '127.0.0.1', '--minimized') -PassThru + Start-Sleep -Seconds 6 + if (Get-Process -Id $proc.Id -ErrorAction SilentlyContinue) { Pass "GUI cold-started and stayed up (minimized to tray)" } + else { Fail "GUI exited or crashed during cold start" } + + # Startup consolidates the cue sounds into UserDataDirectory; with the override that's $testCfg, + # so a populated $testCfg proves the process honoured --config-dir and left the real settings alone. + if (Test-Path (Join-Path $testCfg 'sounds')) { Pass "ran against the isolated --config-dir folder (real settings untouched)" } + else { Fail "--config-dir folder was not populated - config isolation may not be working" } + + Invoke-RsCli @('--close') | Out-Null + Start-Sleep -Seconds 2 + $still = Get-Process -Id $proc.Id -ErrorAction SilentlyContinue + if (-not $still) { Pass "--close shut the GUI down cleanly (no orphan process)" } + else { Fail "process still running after --close"; try { $still | Stop-Process -Force } catch { } } + } + catch { + Fail "cold-start/close smoke threw: $($_.Exception.Message)" + if ($proc) { try { Get-Process -Id $proc.Id -ErrorAction SilentlyContinue | Stop-Process -Force } catch { } } + } + finally { + Remove-Item -LiteralPath $testCfg -Recurse -Force -ErrorAction SilentlyContinue + } +} + # ---- summary ---- Remove-Item -LiteralPath $publishDir -Recurse -Force -ErrorAction SilentlyContinue Write-Host "" diff --git a/src/RemSound.App/CommandLine.cs b/src/RemSound.App/CommandLine.cs index 82d2e8c..757ece2 100644 --- a/src/RemSound.App/CommandLine.cs +++ b/src/RemSound.App/CommandLine.cs @@ -40,6 +40,25 @@ internal static class CommandLine [DllImport("kernel32.dll")] private static extern bool AttachConsole(int dwProcessId); private const int ATTACH_PARENT_PROCESS = -1; + /// The folder given after --config-dir, or null. Read at the very start of + /// - before the layout migration and any config/profile/log/sound access - + /// so it can redirect ALL user state via . + /// Applies to every command (e.g. --selftest --config-dir, --diagnostics --config-dir) + /// and to a normal GUI launch, so a test can exercise a real build without touching live settings. + public static bool TryGetConfigDir(string[] args, out string dir) + { + dir = ""; + for (var i = 0; i < args.Length - 1; i++) + { + if (args[i].Equals("--config-dir", StringComparison.OrdinalIgnoreCase) && !args[i + 1].StartsWith('-')) + { + dir = args[i + 1]; + return !string.IsNullOrWhiteSpace(dir); + } + } + return false; + } + /// /// Process the command line. Returns a non-null exit code when a do-and-exit command ran (the /// caller should Environment.Exit it); returns null to continue into the GUI launch with @@ -61,7 +80,7 @@ internal static class CommandLine return WithConsole(PrintVersion); case "--devices": case "--list-devices": return WithConsole(() => { WriteDevices(Console.Out); return 0; }); - case "--selftest": case "--self-test": + case "--selftest": case "--self-test": case "--smoke-test": case "--smoketest": return WithConsole(() => SelfTest.Run(args)); case "--diagnostics": case "--diag": return WithConsole(() => RunDiagnostics(ValueAfter(args, raw))); @@ -130,7 +149,7 @@ internal static class CommandLine Console.WriteLine(" --devices List all microphones, outputs and ASIO drivers,"); Console.WriteLine(" with their formats and device ids."); Console.WriteLine(" --selftest [--seconds N] Run the built-in self-test - a localhost audio"); - Console.WriteLine(" round-trip plus checks of encryption, the wire format,"); + Console.WriteLine(" (or --smoke-test) round-trip plus checks of encryption, the wire format,"); Console.WriteLine(" settings, profiles and bundled files - and report PASS/FAIL."); Console.WriteLine(" --diagnostics [path] Write a diagnostics report (version, config, profiles,"); Console.WriteLine(" devices, mic-privacy check, recent log) and exit. With"); @@ -145,6 +164,10 @@ internal static class CommandLine Console.WriteLine(" --connect Start and connect to a peer at this address. With no"); Console.WriteLine(" --profile, starts on a fresh profile connected to it."); Console.WriteLine(" --minimized, --tray Start minimized to the notification area."); + Console.WriteLine(" --config-dir Use an explicit folder for this run's settings, profiles,"); + Console.WriteLine(" logs and sounds, instead of the usual location. Lets a test"); + Console.WriteLine(" exercise RemSound without touching your real settings."); + Console.WriteLine(" Works with any command (e.g. --selftest --config-dir ...)."); Console.WriteLine(); Console.WriteLine("Examples:"); Console.WriteLine(" RemSound.exe --devices"); diff --git a/src/RemSound.App/Program.cs b/src/RemSound.App/Program.cs index 627eee2..7f31229 100644 --- a/src/RemSound.App/Program.cs +++ b/src/RemSound.App/Program.cs @@ -25,6 +25,16 @@ internal static class Program return; } + // --config-dir (test / portable isolation): redirect ALL user state - config, + // profiles, logs, cue sounds - to an explicit folder for THIS process only. Applied first, + // before the layout migration and sound consolidation below read or write the default + // location, so a smoke test can run a real build without touching the user's settings + // (smoke-test brief, safety rule 1). + if (CommandLine.TryGetConfigDir(args, out var configDir)) + { + AppConfig.SetUserDataDirectoryOverride(configDir); + } + // SustainedLowLatency tells the GC to avoid full (gen 2) collections while audio is streaming. // Gen 0/1 collections still happen but are sub-millisecond; the long pauses that were causing // the receiver to fall behind in clusters of 4-5 underruns at a time were almost certainly diff --git a/src/RemSound.Core/AppConfig.cs b/src/RemSound.Core/AppConfig.cs index e62786e..65c89b1 100644 --- a/src/RemSound.Core/AppConfig.cs +++ b/src/RemSound.Core/AppConfig.cs @@ -226,7 +226,23 @@ public sealed class AppConfig /// / the earlier config\ folder so the install root stays tidy and the auto-updater can /// exclude one folder to leave ALL user state (including custom cue WAVs) untouched. public const string UserDataFolderName = "user settings and logs"; - public static string UserDataDirectory => Path.Combine(AppContext.BaseDirectory, UserDataFolderName); + + /// Process-wide override for . Null = the default + /// folder next to the exe. Set once at startup from the --config-dir switch so the test + /// suite (and a portable layout) can point ALL user state - config, profiles, logs, cue sounds - + /// at an explicit throwaway folder without touching the user's real settings. Must be set before + /// anything reads config/profiles/logs/sounds. + private static string? _userDataDirectoryOverride; + + /// Redirect every user-state folder to for this process only. + /// Call before / any config read. Idempotent. + public static void SetUserDataDirectoryOverride(string path) + { + if (!string.IsNullOrWhiteSpace(path)) _userDataDirectoryOverride = Path.GetFullPath(path); + } + + public static string UserDataDirectory => + _userDataDirectoryOverride ?? Path.Combine(AppContext.BaseDirectory, UserDataFolderName); /// Where the per-machine log files are written. public static string LogsDirectory => Path.Combine(UserDataDirectory, "logs");