2026-07-12 15:18:27 +01:00
|
|
|
using System.ServiceProcess;
|
|
|
|
|
using RemSound.Core;
|
|
|
|
|
|
|
|
|
|
namespace RemSound.App;
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
/// The RemSound Windows service (send-only lock-screen streaming). Hosts <see cref="ServiceSendHost"/>
|
|
|
|
|
/// on a background thread; the host's own RunLoop yields to the interactive app via
|
|
|
|
|
/// <see cref="InteractivePresence"/>. Started by the SCM when Program.cs is launched with
|
|
|
|
|
/// <see cref="ServiceControl.RunVerb"/>.
|
|
|
|
|
/// </summary>
|
|
|
|
|
public sealed class RemSoundService : ServiceBase
|
|
|
|
|
{
|
|
|
|
|
private readonly CancellationTokenSource cts = new();
|
|
|
|
|
private Thread? worker;
|
|
|
|
|
private ServiceSendHost? host;
|
|
|
|
|
private RemSoundLog? log;
|
2026-07-13 09:38:52 +01:00
|
|
|
private System.Threading.Timer? updateWatch;
|
|
|
|
|
private volatile bool restartScheduled;
|
2026-07-12 15:18:27 +01:00
|
|
|
|
|
|
|
|
public RemSoundService()
|
|
|
|
|
{
|
|
|
|
|
ServiceName = ServiceControl.ServiceName;
|
|
|
|
|
CanStop = true;
|
|
|
|
|
CanShutdown = true;
|
|
|
|
|
CanPauseAndContinue = false;
|
2026-07-13 08:43:33 +01:00
|
|
|
CanHandlePowerEvent = true; // so we can re-open capture after the machine wakes from sleep
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
protected override bool OnPowerEvent(PowerBroadcastStatus powerStatus)
|
|
|
|
|
{
|
|
|
|
|
if (powerStatus is PowerBroadcastStatus.ResumeSuspend or PowerBroadcastStatus.ResumeAutomatic or PowerBroadcastStatus.ResumeCritical)
|
|
|
|
|
{
|
|
|
|
|
log?.Event("service: power resume — re-opening capture");
|
|
|
|
|
try { host?.ReopenAfterResume(); } catch (Exception ex) { log?.Event($"service: resume re-open failed {ex.GetType().Name}: {ex.Message}"); }
|
|
|
|
|
}
|
|
|
|
|
return true;
|
2026-07-12 15:18:27 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
protected override void OnStart(string[] args)
|
|
|
|
|
{
|
|
|
|
|
log = new RemSoundLog { Enabled = SafeServiceLogging() };
|
2026-07-13 09:51:59 +01:00
|
|
|
var version = System.Reflection.Assembly.GetExecutingAssembly().GetName().Version;
|
|
|
|
|
var versionText = version is null ? "?" : $"{version.Major}.{version.Minor}";
|
|
|
|
|
log.Event($"service: OnStart, version {versionText}");
|
|
|
|
|
// Record the running version + start time so the app's Service menu can show them — this is how a
|
|
|
|
|
// self-update is visible (the version bumps and the start time is recent).
|
|
|
|
|
try { ServiceStore.SaveStatus(new ServiceStore.ServiceStatus { Version = versionText, StartedUtc = DateTime.UtcNow }); } catch { }
|
2026-07-13 09:59:40 +01:00
|
|
|
// If this start is the completion of a self-update restart, close the loop in the update log.
|
|
|
|
|
try { if (ServiceStore.ConsumeUpdatePending()) ServiceStore.AppendUpdateLog($"update complete: now running version {versionText}"); } catch { }
|
2026-07-12 15:18:27 +01:00
|
|
|
host = ServiceSendHost.FromConfig(msg => log?.Event(msg));
|
|
|
|
|
worker = new Thread(() =>
|
|
|
|
|
{
|
|
|
|
|
try { host.RunLoop(cts.Token); }
|
|
|
|
|
catch (Exception ex) { log?.Event($"service: run loop crashed {ex.GetType().Name}: {ex.Message}"); }
|
|
|
|
|
})
|
|
|
|
|
{ IsBackground = true, Name = "remsound-service" };
|
|
|
|
|
worker.Start();
|
2026-07-13 09:38:52 +01:00
|
|
|
|
|
|
|
|
// Self-update: the auto-updater swaps the files in place but can't restart us (no admin). We run
|
|
|
|
|
// as SYSTEM, so when a newer RemSound.exe lands next to us we restart onto it ourselves. Checked
|
|
|
|
|
// on a slow timer (an update is rare); loop-safe (only fires on a strictly-newer on-disk version).
|
|
|
|
|
updateWatch = new System.Threading.Timer(_ => CheckForUpdate(), null, TimeSpan.FromSeconds(45), TimeSpan.FromSeconds(45));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private void CheckForUpdate()
|
|
|
|
|
{
|
|
|
|
|
if (restartScheduled) return;
|
|
|
|
|
if (!ServiceUpdate.UpdateLanded()) return;
|
|
|
|
|
restartScheduled = true;
|
2026-07-13 09:59:40 +01:00
|
|
|
var running = ServiceUpdate.RunningVersion();
|
|
|
|
|
var runningText = running is null ? "?" : $"{running.Major}.{running.Minor}";
|
|
|
|
|
// Always-on update log (not gated on the service-logging toggle) — updates are rare + important.
|
|
|
|
|
ServiceStore.AppendUpdateLog($"update detected: newer RemSound.exe ({ServiceUpdate.OnDiskVersion()}) found, running {runningText} — restarting to update");
|
|
|
|
|
ServiceStore.SetUpdatePending();
|
2026-07-13 09:38:52 +01:00
|
|
|
log?.Event("service: a newer RemSound version was installed — restarting to update");
|
|
|
|
|
ServiceUpdate.RestartSelf();
|
2026-07-12 15:18:27 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
protected override void OnStop()
|
|
|
|
|
{
|
|
|
|
|
log?.Event("service: OnStop");
|
2026-07-13 09:38:52 +01:00
|
|
|
try { updateWatch?.Dispose(); } catch { }
|
2026-07-12 15:18:27 +01:00
|
|
|
try { cts.Cancel(); } catch { }
|
|
|
|
|
try { worker?.Join(5000); } catch { }
|
|
|
|
|
try { host?.Dispose(); } catch { }
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
protected override void OnShutdown() => OnStop();
|
|
|
|
|
|
|
|
|
|
private static bool SafeServiceLogging()
|
|
|
|
|
{
|
2026-07-13 08:23:10 +01:00
|
|
|
try { return ServiceStore.LoadLoggingEnabled(); }
|
2026-07-12 15:18:27 +01:00
|
|
|
catch { return false; }
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
protected override void Dispose(bool disposing)
|
|
|
|
|
{
|
|
|
|
|
if (disposing) { try { cts.Dispose(); } catch { } }
|
|
|
|
|
base.Dispose(disposing);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// <summary>Blocks in the SCM dispatcher until the service is stopped. Called from Program.cs
|
|
|
|
|
/// when launched with the run verb.</summary>
|
|
|
|
|
public static void RunAsService() => ServiceBase.Run(new RemSoundService());
|
|
|
|
|
}
|