Service: config dialog + Service menu

Local checkpoint - NOT for public release.

- ServiceProfileDialog: modal 3-tab editor (Audio send / Audio profile / Connectivity)
  reusing the house controls, with a Save and Close / Cancel / Additional options
  button row. Send-only: send-mode chooser + WASAPI outputs/apps + inputs (no "Send
  my audio" toggle, no receive, no ASIO); codec/rate/lock-to-clock; peers add/remove +
  a Set password button. Additional options sub-dialog = play connect/disconnect sound
  + enable service logging. Edits a Profile clone; returns it on OK.
- Service menu in MainForm: status line + Configure / Install / Uninstall / Start / Stop,
  items enabled per live state on drop-down. Install/uninstall confirm then elevate.
  Configure saves the reserved service profile, points AppConfig at it, stores the
  machine-wide logging choice, and restarts a running service to pick up edits.
- ProfileStore.ReservedServiceProfileTitle ("RemSound service"): the service profile
  lives with normal profiles (so the service Loads it) but ListProfileTitles hides it
  from every picker. ServiceControl reuses that single constant.
- Self-test: the service dialog is now in the accessibility audit (constructs cleanly,
  unique mnemonics, all controls named). Gate 20/20.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Ednunp
2026-07-12 15:27:37 +01:00
co-authored by Claude Opus 4.8
parent 5dbbe4dd71
commit f4551517e5
5 changed files with 495 additions and 2 deletions
+100
View File
@@ -2159,10 +2159,110 @@ public sealed class MainForm : Form
menu.Items.Add(fileMenu);
menu.Items.Add(recordMenu);
menu.Items.Add(optionsMenu);
menu.Items.Add(BuildServiceMenu());
menu.Items.Add(helpMenu);
return menu;
}
/// <summary>The Service menu: configure the send-only lock-screen service's profile, and
/// install / uninstall / start / stop it. The status line and item enabling refresh each time the
/// menu opens. Install/uninstall/start/stop re-launch RemSound elevated (one UAC prompt each);
/// querying status needs no elevation.</summary>
private ToolStripMenuItem BuildServiceMenu()
{
var serviceMenu = new ToolStripMenuItem("&Service") { AccessibleName = "Service menu" };
var status = new ToolStripMenuItem("Service: …") { Enabled = false, AccessibleName = "Service status" };
var configure = new ToolStripMenuItem("&Configure service profile...") { AccessibleName = "Configure service profile" };
configure.Click += (_, _) => ConfigureServiceProfile();
var install = new ToolStripMenuItem("&Install service") { AccessibleName = "Install service" };
install.Click += (_, _) => ServiceAction(ServiceControl.InstallVerb, "install", confirm: true);
var uninstall = new ToolStripMenuItem("&Uninstall service") { AccessibleName = "Uninstall service" };
uninstall.Click += (_, _) => ServiceAction(ServiceControl.UninstallVerb, "uninstall", confirm: true);
var start = new ToolStripMenuItem("S&tart service") { AccessibleName = "Start service" };
start.Click += (_, _) => ServiceAction(ServiceControl.StartVerb, "start", confirm: false);
var stop = new ToolStripMenuItem("Sto&p service") { AccessibleName = "Stop service" };
stop.Click += (_, _) => ServiceAction(ServiceControl.StopVerb, "stop", confirm: false);
serviceMenu.DropDownItems.AddRange(new ToolStripItem[]
{
status, new ToolStripSeparator(),
configure, new ToolStripSeparator(),
install, uninstall, start, stop,
});
serviceMenu.DropDownOpening += (_, _) =>
{
var state = ServiceControl.Query();
status.Text = "Service: " + DescribeServiceState(state);
var installed = state != ServiceState.NotInstalled;
install.Enabled = !installed;
uninstall.Enabled = installed;
start.Enabled = installed && state is ServiceState.Stopped;
stop.Enabled = installed && state is ServiceState.Running;
};
return serviceMenu;
}
private static string DescribeServiceState(ServiceState s) => s switch
{
ServiceState.NotInstalled => "not installed",
ServiceState.Running => "installed, running",
ServiceState.Stopped => "installed, stopped",
ServiceState.StartPending => "starting…",
ServiceState.StopPending => "stopping…",
_ => "unknown",
};
/// <summary>Opens the modal service-profile editor, then persists the result: saves the reserved
/// service profile, points AppConfig at it, and stores the machine-wide service-logging choice. If
/// the service is running, restarts it so the edits take effect.</summary>
private void ConfigureServiceProfile()
{
if (profileStore is null) return;
var cfg = AppConfig.Load();
Profile current;
try { current = profileStore.Load(ServiceControl.ServiceProfileTitle) ?? Profile.NewBlank(); }
catch { current = Profile.NewBlank(); }
using var dlg = new ServiceProfileDialog(current, cfg.ServiceLoggingEnabled);
if (dlg.ShowDialog(this) != DialogResult.OK) return;
try
{
profileStore.Save(dlg.Result);
var c = AppConfig.Load();
c.ServiceProfileName = ServiceControl.ServiceProfileTitle;
c.ServiceLoggingEnabled = dlg.ServiceLoggingEnabled;
c.Save();
if (ServiceControl.Query() == ServiceState.Running)
{
ServiceControl.RunElevated(ServiceControl.StopVerb);
ServiceControl.RunElevated(ServiceControl.StartVerb);
}
MessageBox.Show(this, "Service profile saved.", AppName, MessageBoxButtons.OK, MessageBoxIcon.Information);
}
catch (Exception ex)
{
MessageBox.Show(this, $"Could not save the service profile: {ex.Message}", AppName, MessageBoxButtons.OK, MessageBoxIcon.Warning);
}
}
private void ServiceAction(string verb, string label, bool confirm)
{
if (confirm)
{
var msg = verb == ServiceControl.InstallVerb
? "Install the RemSound send-only service? It will start automatically at boot and stream your service profile whenever you're not using RemSound normally.\n\nWindows will ask for administrator permission."
: "Uninstall the RemSound service?\n\nWindows will ask for administrator permission.";
if (MessageBox.Show(this, msg, AppName, MessageBoxButtons.YesNo, MessageBoxIcon.Question) != DialogResult.Yes) return;
}
var rc = ServiceControl.RunElevated(verb);
if (rc == 0)
MessageBox.Show(this, $"Service {label} succeeded.", AppName, MessageBoxButtons.OK, MessageBoxIcon.Information);
else if (rc == -1)
MessageBox.Show(this, $"Service {label} was cancelled, or administrator rights were declined.", AppName, MessageBoxButtons.OK, MessageBoxIcon.Warning);
else
MessageBox.Show(this, $"Service {label} failed (code {rc}).", AppName, MessageBoxButtons.OK, MessageBoxIcon.Warning);
}
/// <summary>Rebuild the Recent profiles submenu from <see cref="AppConfig.RecentProfiles"/>.
/// Called once during menu construction (so it's not visibly empty before the first
/// open) and on every DropDownOpening so the latest list is always shown. Entries that