diff --git a/DesktopBridge/SmartThings.Service/TrayService.cs b/DesktopBridge/SmartThings.Service/TrayService.cs
index 535ad05..9619c45 100644
--- a/DesktopBridge/SmartThings.Service/TrayService.cs
+++ b/DesktopBridge/SmartThings.Service/TrayService.cs
@@ -8,8 +8,20 @@ namespace TrayLauncherService
/// logon it launches, in the interactive user session:
/// - the native TrayHelper (stays running - owns the tray icon), and
/// - the WPF process (only needed briefly).
- /// After a short delay it kills WPF, then stops itself (its work is done). It is auto-start, so
- /// it runs again on the next boot; if no user is signed in yet it waits for logon.
+ /// After a short delay it kills WPF, then STOPS ITSELF - its work is done, so it consumes no
+ /// memory while idle.
+ ///
+ /// FAST STARTUP: Windows Fast Startup (the default for "Shut down") hibernates session 0 and
+ /// restores it on the next power-on instead of doing a cold boot, so auto-start services are NOT
+ /// re-run. A self-stopped service therefore stays stopped after a shutdown+power-on and would
+ /// never see the logon (a full "Restart" bypasses Fast Startup, which is why restarting worked
+ /// but shutdown+power-on did not). That gap is covered by the manifest windows.startupTask
+ /// (SmartThings.WAPP/Package.appxmanifest), which launches the tray helper directly at every
+ /// logon - including the logon after a Fast-Startup resume. The tray helper is single-instance,
+ /// so a service-initiated launch and a startup-task launch never produce two icons.
+ ///
+ /// This service still handles install-time and cold-boot launches (and preloaded devices, where
+ /// the app may never be opened and the startup task's "run once" gate would otherwise apply).
///
public sealed class TrayService : ServiceBase
{
@@ -22,6 +34,10 @@ public sealed class TrayService : ServiceBase
// Process image name (no .exe) of the player - the AssemblyName of SmartThings.AVplayer.
private const string PlayerProcessName = "SmartThings.AVplayer";
+ // Process image name (no .exe) of the native tray helper - the Executable in the manifest's
+ // SmartThings.Tray application entry. Used to avoid launching a second tray icon.
+ private const string TrayProcessName = "SmartThings.Tray";
+
public TrayService()
{
ServiceName = ServiceNameConst;
@@ -63,11 +79,20 @@ protected override void OnSessionChange(SessionChangeDescription changeDescripti
}
///
- /// Launches TrayHelper + WPF in the session, then (after a delay) kills WPF and stops the
- /// service. Only proceeds to kill/stop if the tray helper actually launched.
+ /// Launches the tray helper (+ WPF) in the session if it isn't already there, kills WPF after
+ /// a short delay, then stops the service. If the tray is already running in the session this
+ /// is a no-op except for stopping the service. The logon task (registered in OnStart) restarts
+ /// the service on the next sign-in, including after a Fast-Startup resume.
///
private void LaunchThenStop(uint sessionId)
{
+ if (IsTrayRunningInSession(sessionId))
+ {
+ ServiceLog.Write($"Tray already running in session {sessionId}; nothing to launch.");
+ StopSelf();
+ return;
+ }
+
bool trayLaunched = SessionLauncher.LaunchInSession(sessionId, SessionLauncher.TrayAlias);
bool wpfLaunched = SessionLauncher.LaunchInSession(sessionId, SessionLauncher.WpfAlias);
ServiceLog.Write($"Launch results: tray={trayLaunched}, wpf={wpfLaunched}.");
@@ -85,6 +110,41 @@ private void LaunchThenStop(uint sessionId)
StopSelf();
}
+ ///
+ /// True if a tray helper process is already running in the given session. The service runs in
+ /// session 0 and can see processes in every session, so we filter by SessionId to avoid
+ /// treating a tray in another user's session as ours.
+ ///
+ private static bool IsTrayRunningInSession(uint sessionId)
+ {
+ Process[] trays = Process.GetProcessesByName(TrayProcessName);
+ try
+ {
+ foreach (Process process in trays)
+ {
+ try
+ {
+ if ((uint)process.SessionId == sessionId)
+ {
+ return true;
+ }
+ }
+ catch
+ {
+ // Process may have exited between enumeration and access; ignore it.
+ }
+ }
+ return false;
+ }
+ finally
+ {
+ foreach (Process process in trays)
+ {
+ process.Dispose();
+ }
+ }
+ }
+
///
/// Terminates the player process (image name "SmartThings.AVplayer.exe"). LocalSystem can
/// terminate the user's process. Best effort.
@@ -120,8 +180,9 @@ protected override void OnStop()
}
///
- /// Stops this service (nothing left to do after launching the tray). Runs on a background
- /// thread, so the service has already reported Running to the SCM.
+ /// Stops this service (nothing left to do after launching the tray - the logon task will
+ /// restart it on the next sign-in). Runs on a background thread, so the service has already
+ /// reported Running to the SCM.
///
private void StopSelf()
{
diff --git a/DesktopBridge/SmartThings.WAPP/Package.appxmanifest b/DesktopBridge/SmartThings.WAPP/Package.appxmanifest
index 6bdf0ed..381d81a 100644
--- a/DesktopBridge/SmartThings.WAPP/Package.appxmanifest
+++ b/DesktopBridge/SmartThings.WAPP/Package.appxmanifest
@@ -109,6 +109,21 @@
+
+
+
+