Service owns its binaries + is stoppable without admin; installer offers the service
The service was registered to run from wherever it was installed FROM (Environment .ProcessPath), so installing from a dev/test folder pinned it there: it locked those files (blocking every rebuild) and, for a real user installing from the app folder, would lock the app's own binaries and break the auto-updater. Stopping it also needed admin, so only the app's UAC-prompting Service menu could do it. Fixes: - The service now installs its OWN copy of the program into ProgramData\RemSound\ service\bin and is registered to run from there. Never touches the install/dev folder again. CopyProgramTo copies the exe + DLLs + runtimes + default sounds but excludes user-state folders; uninstall removes the bin copy. - Install grants Authenticated Users start/stop/query on the service (sc sdset, ACE merged into the existing DACL) so it can be stopped with a plain `sc stop RemSoundService` or the Service menu -- no admin, no app. Pure SDDL-amend helper is unit-tested (placement + idempotency). - The app installer now asks, after install, whether to also install the service (optional, its own UAC step; declining is fine -- Service menu installs it later). - deploy-test.ps1: only a copy running FROM the publish folder locks its binaries, so only that forces a sounds-only deploy. The service (ProgramData) and an installed app no longer make the script silently skip the binary publish -- the bug that had us testing stale builds for rounds. New self-test "Service self-contained install" (runs-from-own-bin, SDDL amend, copy exclusions). Gate: 40/40. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.8
parent
a450cae66a
commit
4e15451e1d
+11
-2
@@ -33,9 +33,18 @@ function Invoke-Robocopy([string[]]$rcArgs) {
|
|||||||
if ($LASTEXITCODE -ge 8) { throw "robocopy failed ($LASTEXITCODE): $($rcArgs -join ' ')" }
|
if ($LASTEXITCODE -ge 8) { throw "robocopy failed ($LASTEXITCODE): $($rcArgs -join ' ')" }
|
||||||
}
|
}
|
||||||
|
|
||||||
$soundOnly = @(Get-Process RemSound -ErrorAction SilentlyContinue).Count -gt 0
|
# Only a RemSound process running FROM the publish folder locks its binaries. The send-only Windows
|
||||||
|
# service runs from its own copy under ProgramData (...\service\bin) and an installed app runs from
|
||||||
|
# %LocalAppData%\Programs\RemSound - neither locks publish, so neither should force a sounds-only deploy.
|
||||||
|
# (This is the bug that silently skipped binary deploys while the service was running - 2026-07-17.)
|
||||||
|
$publishFull = (Resolve-Path -LiteralPath $publish -ErrorAction SilentlyContinue).Path
|
||||||
|
$locking = @(Get-Process RemSound -ErrorAction SilentlyContinue | Where-Object {
|
||||||
|
try { $_.Path -and $publishFull -and $_.Path.StartsWith($publishFull, [System.StringComparison]::OrdinalIgnoreCase) }
|
||||||
|
catch { $false } # .Path throws for the SYSTEM service (access denied) - it's not in publish, so ignore it
|
||||||
|
})
|
||||||
|
$soundOnly = $locking.Count -gt 0
|
||||||
if ($soundOnly) {
|
if ($soundOnly) {
|
||||||
Write-Host "RemSound is running - refreshing SOUNDS only (binaries are locked; close RemSound to update them)." -ForegroundColor Yellow
|
Write-Host "A RemSound is running FROM the publish folder - refreshing SOUNDS only (its binaries are locked; close that copy to update them)." -ForegroundColor Yellow
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
# Never deploy a build to test that hasn't passed the tests. The gate publishes + tests its own
|
# Never deploy a build to test that hasn't passed the tests. The gate publishes + tests its own
|
||||||
|
|||||||
@@ -170,6 +170,33 @@ internal static class AppInstaller
|
|||||||
"Press OK to finish. RemSound will now close and reopen from the new install location.",
|
"Press OK to finish. RemSound will now close and reopen from the new install location.",
|
||||||
"RemSound installed", MessageBoxButtons.OK, MessageBoxIcon.Information);
|
"RemSound installed", MessageBoxButtons.OK, MessageBoxIcon.Information);
|
||||||
|
|
||||||
|
// Offer to install the send-only Windows service too (Ed, 2026-07-17). It's a separate, optional
|
||||||
|
// component that needs its own admin (UAC) step, so we ask rather than assume. Declining is fine —
|
||||||
|
// it can be installed any time from the app's Service menu. Never block the app install on it.
|
||||||
|
try
|
||||||
|
{
|
||||||
|
if (!ServiceControl.IsInstalled())
|
||||||
|
{
|
||||||
|
var wantService = MessageBox.Show(owner,
|
||||||
|
"Do you also want to install the RemSound service?" + Environment.NewLine + Environment.NewLine +
|
||||||
|
"The service streams this PC's audio to your RemSound peers even when nobody is logged in " +
|
||||||
|
"(for example at the lock screen after a reboot). It's send-only and steps aside whenever the " +
|
||||||
|
"RemSound app is open. You can install or remove it later from the app's Service menu.",
|
||||||
|
"Install the RemSound service?", MessageBoxButtons.YesNo, MessageBoxIcon.Question);
|
||||||
|
if (wantService == DialogResult.Yes)
|
||||||
|
{
|
||||||
|
log?.Invoke("install: user opted to install the service too");
|
||||||
|
var rc = ServiceControl.RunElevated(ServiceControl.InstallVerb);
|
||||||
|
MessageBox.Show(owner,
|
||||||
|
rc == 0
|
||||||
|
? "The RemSound service was installed. Configure and start it from the app's Service menu when you want it running."
|
||||||
|
: "The RemSound service was not installed (the elevation prompt was declined, or it failed). You can try again later from the app's Service menu.",
|
||||||
|
"RemSound service", MessageBoxButtons.OK, rc == 0 ? MessageBoxIcon.Information : MessageBoxIcon.Warning);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception ex) { log?.Invoke($"install: optional service step skipped ({ex.GetType().Name}: {ex.Message})"); }
|
||||||
|
|
||||||
// Hand over cleanly. We can't just launch the installed exe and exit: the single-instance
|
// Hand over cleanly. We can't just launch the installed exe and exit: the single-instance
|
||||||
// lock would still be held for the instant it takes us to shut down, and the new copy would
|
// lock would still be held for the instant it takes us to shut down, and the new copy would
|
||||||
// see "already running". So a tiny batch waits for THIS process to exit (lock released), then
|
// see "already running". So a tiny batch waits for THIS process to exit (lock released), then
|
||||||
|
|||||||
@@ -73,6 +73,7 @@ internal static class SelfTest
|
|||||||
RunStep(results, "Remembered applications list is global + clearable", RememberedApplicationsGlobal);
|
RunStep(results, "Remembered applications list is global + clearable", RememberedApplicationsGlobal);
|
||||||
RunStep(results, "Send-app lists semantics (ticked → Active, out of Remembered)", SendAppListSemantics);
|
RunStep(results, "Send-app lists semantics (ticked → Active, out of Remembered)", SendAppListSemantics);
|
||||||
RunStep(results, "Service registration args", ServiceRegistrationArgs);
|
RunStep(results, "Service registration args", ServiceRegistrationArgs);
|
||||||
|
RunStep(results, "Service self-contained install (own bin + user stop rights)", ServiceSelfContainedInstall);
|
||||||
RunStep(results, "Recording engine (all formats + source gate + mono)", RecordingEngine);
|
RunStep(results, "Recording engine (all formats + source gate + mono)", RecordingEngine);
|
||||||
RunStep(results, "Recording split tracks (per-peer + own)", RecordingSplitTracks);
|
RunStep(results, "Recording split tracks (per-peer + own)", RecordingSplitTracks);
|
||||||
RunStep(results, "Recording churn / soak", RecordingChurn);
|
RunStep(results, "Recording churn / soak", RecordingChurn);
|
||||||
@@ -742,6 +743,61 @@ internal static class SelfTest
|
|||||||
return "sc create + failure args well-formed; self-update comparison loop-safe";
|
return "sc create + failure args well-formed; self-update comparison loop-safe";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>The service installs and runs from its OWN copy of the program under ProgramData, never the
|
||||||
|
/// folder it was installed from — so it can't lock the app's install folder / a dev working copy or
|
||||||
|
/// block the auto-updater. And it grants authenticated users start/stop so it's stoppable without admin.
|
||||||
|
/// Tests the pure pieces: the run-from path, the SDDL amendment, and the program-copy exclusions.</summary>
|
||||||
|
private static string? ServiceSelfContainedInstall()
|
||||||
|
{
|
||||||
|
// 1. The service runs from ProgramData\RemSound\service\bin\RemSound.exe, and BuildCreateArgs points there.
|
||||||
|
var programData = Environment.GetFolderPath(Environment.SpecialFolder.CommonApplicationData);
|
||||||
|
Check(ServiceStore.BinExePath.StartsWith(programData, StringComparison.OrdinalIgnoreCase)
|
||||||
|
&& ServiceStore.BinExePath.EndsWith(@"\bin\RemSound.exe", StringComparison.OrdinalIgnoreCase),
|
||||||
|
$"the service must run from its own ProgramData bin copy (got {ServiceStore.BinExePath})");
|
||||||
|
var createArgs = ServiceControl.BuildCreateArgs(ServiceStore.BinExePath);
|
||||||
|
Check(createArgs.Contains("\\\"" + ServiceStore.BinExePath + "\\\" " + ServiceControl.RunVerb),
|
||||||
|
"the create command must register the ProgramData bin exe as the service binary");
|
||||||
|
|
||||||
|
// 2. AddUserStartStopAce inserts the AU start/stop ACE into the DACL, ahead of the SACL, and is idempotent.
|
||||||
|
const string sample = "D:(A;;CCLCSWRPWPDTLOCRRC;;;SY)(A;;CCLCSWLOCRRC;;;IU)S:(AU;FA;CCDCLCSWRPWPDTLOCRSDRCWDWO;;;WD)";
|
||||||
|
var amended = ServiceControl.AddUserStartStopAce(sample);
|
||||||
|
Check(amended is not null && amended.Contains(ServiceControl.UserStartStopAce), "the AU start/stop ACE must be added");
|
||||||
|
Check(amended!.IndexOf(ServiceControl.UserStartStopAce, StringComparison.Ordinal) < amended.IndexOf("S:", StringComparison.Ordinal),
|
||||||
|
"the ACE must sit inside the DACL, before the SACL");
|
||||||
|
Check(amended.StartsWith("D:", StringComparison.Ordinal), "the result must still be a valid DACL-first SDDL");
|
||||||
|
Check(ServiceControl.AddUserStartStopAce(amended) == amended, "adding the ACE twice must be a no-op (idempotent)");
|
||||||
|
Check(ServiceControl.AddUserStartStopAce("garbage") is null, "a non-DACL SDDL must be rejected");
|
||||||
|
|
||||||
|
// 3. CopyProgramTo copies program files but NEVER the user-state folders.
|
||||||
|
var root = Path.Combine(Path.GetTempPath(), "remsound-svccopy-" + Guid.NewGuid().ToString("N"));
|
||||||
|
var src = Path.Combine(root, "src");
|
||||||
|
var dst = Path.Combine(root, "dst");
|
||||||
|
try
|
||||||
|
{
|
||||||
|
Directory.CreateDirectory(Path.Combine(src, "runtimes", "win-x64", "native"));
|
||||||
|
Directory.CreateDirectory(Path.Combine(src, "default sounds"));
|
||||||
|
Directory.CreateDirectory(Path.Combine(src, "user settings and logs", "logs"));
|
||||||
|
Directory.CreateDirectory(Path.Combine(src, "logs"));
|
||||||
|
File.WriteAllText(Path.Combine(src, "RemSound.exe"), "exe");
|
||||||
|
File.WriteAllText(Path.Combine(src, "RemSound.Sender.dll"), "dll");
|
||||||
|
File.WriteAllText(Path.Combine(src, "runtimes", "win-x64", "native", "opus.dll"), "opus");
|
||||||
|
File.WriteAllText(Path.Combine(src, "default sounds", "connect.wav"), "wav");
|
||||||
|
File.WriteAllText(Path.Combine(src, "user settings and logs", "logs", "secret.log"), "log");
|
||||||
|
File.WriteAllText(Path.Combine(src, "logs", "stray.log"), "log");
|
||||||
|
|
||||||
|
ServiceControl.CopyProgramTo(src, dst);
|
||||||
|
|
||||||
|
Check(File.Exists(Path.Combine(dst, "RemSound.exe")), "the exe must be copied");
|
||||||
|
Check(File.Exists(Path.Combine(dst, "RemSound.Sender.dll")), "sibling DLLs must be copied");
|
||||||
|
Check(File.Exists(Path.Combine(dst, "runtimes", "win-x64", "native", "opus.dll")), "native runtimes must be copied");
|
||||||
|
Check(File.Exists(Path.Combine(dst, "default sounds", "connect.wav")), "bundled default sounds must be copied");
|
||||||
|
Check(!Directory.Exists(Path.Combine(dst, "user settings and logs")), "user settings/logs must NOT be copied");
|
||||||
|
Check(!Directory.Exists(Path.Combine(dst, "logs")), "stray logs folder must NOT be copied");
|
||||||
|
return "runs from own ProgramData bin; AU start/stop ACE added idempotently; program copy excludes user state";
|
||||||
|
}
|
||||||
|
finally { try { Directory.Delete(root, recursive: true); } catch { /* temp */ } }
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>The service profile is fully isolated from the normal profile machinery: it lives in a
|
/// <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
|
/// 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
|
/// folder), and the reserved title never shows up in the profile listing that backs the startup
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
using System.ComponentModel;
|
using System.ComponentModel;
|
||||||
using System.Diagnostics;
|
using System.Diagnostics;
|
||||||
|
using System.IO;
|
||||||
using System.ServiceProcess;
|
using System.ServiceProcess;
|
||||||
|
using RemSound.Core;
|
||||||
|
|
||||||
namespace RemSound.App;
|
namespace RemSound.App;
|
||||||
|
|
||||||
@@ -86,24 +88,100 @@ public static class ServiceControl
|
|||||||
|
|
||||||
// ---- Elevated-side (called from Program.cs when running an --xxx-service verb) ---------------
|
// ---- Elevated-side (called from Program.cs when running an --xxx-service verb) ---------------
|
||||||
|
|
||||||
/// <summary>Creates the service (auto-start) pointing at this exe with <see cref="RunVerb"/>. Must be
|
/// <summary>Installs the service. Must be run elevated. Copies the program to the service's OWN folder
|
||||||
/// run elevated. Returns 0 on success. Idempotent-ish: if it already exists, reports success.</summary>
|
/// (<see cref="ServiceStore.BinDirectory"/>) and registers it to run from THERE — never from the app's
|
||||||
|
/// install folder or a dev working copy — so it can't lock those files or block the app's auto-updater.
|
||||||
|
/// Also grants the machine's authenticated users start/stop rights, so the service can be stopped with
|
||||||
|
/// a plain <c>sc stop</c> (no admin, no app). Returns 0 on success. Idempotent-ish: already-installed
|
||||||
|
/// reports success.</summary>
|
||||||
public static int DoInstall()
|
public static int DoInstall()
|
||||||
{
|
{
|
||||||
if (IsInstalled()) return 0;
|
if (IsInstalled()) return 0;
|
||||||
var exe = Environment.ProcessPath;
|
var exe = Environment.ProcessPath;
|
||||||
if (string.IsNullOrEmpty(exe)) return 2;
|
if (string.IsNullOrEmpty(exe)) return 2;
|
||||||
|
var sourceDir = Path.GetDirectoryName(exe);
|
||||||
|
if (string.IsNullOrEmpty(sourceDir)) return 2;
|
||||||
|
|
||||||
var rc = RunSc(BuildCreateArgs(exe));
|
// Copy the whole program (exe + DLLs + runtimes/ + default sounds/) into the service's own bin
|
||||||
|
// folder, so the running service uses ITS copy, not the source it was installed from.
|
||||||
|
try { CopyProgramTo(sourceDir, ServiceStore.BinDirectory); }
|
||||||
|
catch (Exception ex) { ServiceStore.AppendServiceEvent($"install: copy program failed: {ex.GetType().Name}: {ex.Message}"); return 5; }
|
||||||
|
|
||||||
|
var rc = RunSc(BuildCreateArgs(ServiceStore.BinExePath));
|
||||||
if (rc != 0) return rc;
|
if (rc != 0) return rc;
|
||||||
// Best-effort description; failure here doesn't fail the install.
|
// Best-effort description; failure here doesn't fail the install.
|
||||||
RunSc($"description {ServiceName} \"{Description}\"");
|
RunSc($"description {ServiceName} \"{Description}\"");
|
||||||
// Auto-restart on crash: without this a crashed service stays dead until reboot, which defeats
|
// Auto-restart on crash: without this a crashed service stays dead until reboot, which defeats
|
||||||
// an always-on streamer. Restart 5s / 10s / then every 60s; reset the failure counter daily.
|
// an always-on streamer. Restart 5s / 10s / then every 60s; reset the failure counter daily.
|
||||||
RunSc(BuildFailureArgs());
|
RunSc(BuildFailureArgs());
|
||||||
|
// Let a normal (non-admin) user start/stop it — otherwise stopping needs the app's UAC prompt.
|
||||||
|
GrantUserStartStop();
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>Copies the program files from <paramref name="sourceDir"/> to <paramref name="destDir"/>,
|
||||||
|
/// recursively, but NEVER the user-state folders (logs, profiles, config, recordings) — the service
|
||||||
|
/// keeps its own state in ProgramData. Overwrites so a re-install refreshes the binaries.</summary>
|
||||||
|
internal static void CopyProgramTo(string sourceDir, string destDir)
|
||||||
|
{
|
||||||
|
// Guard against copying a folder onto itself (re-install from the service bin folder).
|
||||||
|
if (string.Equals(Path.GetFullPath(sourceDir).TrimEnd('\\'),
|
||||||
|
Path.GetFullPath(destDir).TrimEnd('\\'), StringComparison.OrdinalIgnoreCase))
|
||||||
|
return;
|
||||||
|
|
||||||
|
var skipDirs = new HashSet<string>(StringComparer.OrdinalIgnoreCase)
|
||||||
|
{ "user settings and logs", "logs", "recordings", "profiles", "config" };
|
||||||
|
|
||||||
|
static void CopyDir(string src, string dst, HashSet<string> skip)
|
||||||
|
{
|
||||||
|
Directory.CreateDirectory(dst);
|
||||||
|
foreach (var file in Directory.GetFiles(src))
|
||||||
|
File.Copy(file, Path.Combine(dst, Path.GetFileName(file)), overwrite: true);
|
||||||
|
foreach (var dir in Directory.GetDirectories(src))
|
||||||
|
{
|
||||||
|
var name = Path.GetFileName(dir);
|
||||||
|
if (skip.Contains(name)) continue;
|
||||||
|
CopyDir(dir, Path.Combine(dst, name), skip);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
CopyDir(sourceDir, destDir, skipDirs);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Adds an ACE granting Authenticated Users start + stop + query on the service, so the
|
||||||
|
/// service can be stopped/started without administrator rights (a plain <c>sc stop RemSoundService</c>
|
||||||
|
/// or the app's Service menu without a UAC prompt). Reads the current security descriptor and inserts
|
||||||
|
/// the ACE, so nothing already granted is lost. Best-effort — a failure just leaves the default
|
||||||
|
/// (admin-only) rights in place.</summary>
|
||||||
|
private static void GrantUserStartStop()
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var sddl = RunScCapture($"sdshow {ServiceName}").Trim();
|
||||||
|
var newSddl = AddUserStartStopAce(sddl);
|
||||||
|
if (newSddl is null || string.Equals(newSddl, sddl, StringComparison.Ordinal)) return;
|
||||||
|
var rc = RunSc($"sdset {ServiceName} {newSddl}");
|
||||||
|
if (rc != 0) ServiceStore.AppendServiceEvent($"install: sdset (user start/stop) returned {rc}");
|
||||||
|
}
|
||||||
|
catch (Exception ex) { ServiceStore.AppendServiceEvent($"install: grant user start/stop failed: {ex.GetType().Name}: {ex.Message}"); }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>The ACE granting Authenticated Users start (RP) + stop (WP) + query status (LC) + read
|
||||||
|
/// control (RC). Public-ish for the self-test.</summary>
|
||||||
|
internal const string UserStartStopAce = "(A;;RPWPLCRC;;;AU)";
|
||||||
|
|
||||||
|
/// <summary>Pure, testable: insert <see cref="UserStartStopAce"/> into a service SDDL's DACL (right
|
||||||
|
/// after "D:" and any DACL flags, ahead of the first ACE and the SACL). Returns null for an SDDL that
|
||||||
|
/// doesn't start with a DACL, and the input unchanged if the ACE is already present.</summary>
|
||||||
|
internal static string? AddUserStartStopAce(string? sddl)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrEmpty(sddl) || !sddl.StartsWith("D:", StringComparison.Ordinal)) return null;
|
||||||
|
if (sddl.Contains(UserStartStopAce, StringComparison.OrdinalIgnoreCase)) return sddl; // already granted
|
||||||
|
var firstAce = sddl.IndexOf('(');
|
||||||
|
var sacl = sddl.IndexOf("S:", StringComparison.Ordinal);
|
||||||
|
var insertAt = firstAce >= 0 && (sacl < 0 || firstAce < sacl) ? firstAce : (sacl >= 0 ? sacl : sddl.Length);
|
||||||
|
return sddl.Insert(insertAt, UserStartStopAce);
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>The sc.exe "failure" args that make the service auto-restart on a crash. Pure, so a
|
/// <summary>The sc.exe "failure" args that make the service auto-restart on a crash. Pure, so a
|
||||||
/// self-test can verify the format.</summary>
|
/// self-test can verify the format.</summary>
|
||||||
internal static string BuildFailureArgs() =>
|
internal static string BuildFailureArgs() =>
|
||||||
@@ -115,7 +193,12 @@ public static class ServiceControl
|
|||||||
{
|
{
|
||||||
if (!IsInstalled()) return 0;
|
if (!IsInstalled()) return 0;
|
||||||
try { DoStop(); } catch { /* best-effort */ }
|
try { DoStop(); } catch { /* best-effort */ }
|
||||||
return RunSc($"delete {ServiceName}");
|
var rc = RunSc($"delete {ServiceName}");
|
||||||
|
// Remove the service's own copy of the program (best-effort; a failure — e.g. a file still briefly
|
||||||
|
// locked as the service exits — just leaves a stale bin folder, which a re-install overwrites).
|
||||||
|
try { if (Directory.Exists(ServiceStore.BinDirectory)) Directory.Delete(ServiceStore.BinDirectory, recursive: true); }
|
||||||
|
catch (Exception ex) { ServiceStore.AppendServiceEvent($"uninstall: could not remove bin folder: {ex.GetType().Name}: {ex.Message}"); }
|
||||||
|
return rc;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>Starts the service. Must be run elevated. Returns 0 on success.</summary>
|
/// <summary>Starts the service. Must be run elevated. Returns 0 on success.</summary>
|
||||||
@@ -167,6 +250,30 @@ public static class ServiceControl
|
|||||||
catch { return 4; }
|
catch { return 4; }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>Runs sc.exe and returns its stdout (empty on failure). Used to read the service's security
|
||||||
|
/// descriptor (<c>sdshow</c>) before amending it.</summary>
|
||||||
|
private static string RunScCapture(string arguments)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var psi = new ProcessStartInfo
|
||||||
|
{
|
||||||
|
FileName = "sc.exe",
|
||||||
|
Arguments = arguments,
|
||||||
|
UseShellExecute = false,
|
||||||
|
CreateNoWindow = true,
|
||||||
|
RedirectStandardOutput = true,
|
||||||
|
RedirectStandardError = true,
|
||||||
|
};
|
||||||
|
using var p = Process.Start(psi);
|
||||||
|
if (p is null) return "";
|
||||||
|
var stdout = p.StandardOutput.ReadToEnd();
|
||||||
|
p.WaitForExit(20000);
|
||||||
|
return stdout;
|
||||||
|
}
|
||||||
|
catch { return ""; }
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>Builds the exact sc.exe "create" argument string for a given exe path. Pure and
|
/// <summary>Builds the exact sc.exe "create" argument string for a given exe path. Pure and
|
||||||
/// side-effect-free so a self-test can verify the fiddly quoting without touching the SCM.
|
/// side-effect-free so a self-test can verify the fiddly quoting without touching the SCM.
|
||||||
///
|
///
|
||||||
|
|||||||
@@ -27,6 +27,15 @@ public static class ServiceStore
|
|||||||
public static string Directory => TestDirectoryOverride ?? Path.Combine(
|
public static string Directory => TestDirectoryOverride ?? Path.Combine(
|
||||||
Environment.GetFolderPath(Environment.SpecialFolder.CommonApplicationData), "RemSound", "service");
|
Environment.GetFolderPath(Environment.SpecialFolder.CommonApplicationData), "RemSound", "service");
|
||||||
|
|
||||||
|
/// <summary>ProgramData\RemSound\service\bin — the service's OWN copy of the program. The service is
|
||||||
|
/// registered to run from here, NOT from wherever it was installed from, so it never locks the app's
|
||||||
|
/// install folder or a developer's working copy (which used to block every rebuild and break the app's
|
||||||
|
/// auto-updater). Install copies the binaries here; uninstall removes them.</summary>
|
||||||
|
public static string BinDirectory => Path.Combine(Directory, "bin");
|
||||||
|
|
||||||
|
/// <summary>Full path to the service's own RemSound.exe under <see cref="BinDirectory"/>.</summary>
|
||||||
|
public static string BinExePath => Path.Combine(BinDirectory, "RemSound.exe");
|
||||||
|
|
||||||
private static string ProfilePath => Path.Combine(Directory, "service-profile.json");
|
private static string ProfilePath => Path.Combine(Directory, "service-profile.json");
|
||||||
private static string SettingsPath => Path.Combine(Directory, "service-settings.json");
|
private static string SettingsPath => Path.Combine(Directory, "service-settings.json");
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user