Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
73 changes: 67 additions & 6 deletions DesktopBridge/SmartThings.Service/TrayService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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).
/// </summary>
public sealed class TrayService : ServiceBase
{
Expand All @@ -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;
Expand Down Expand Up @@ -63,11 +79,20 @@ protected override void OnSessionChange(SessionChangeDescription changeDescripti
}

/// <summary>
/// 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.
/// </summary>
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}.");
Expand All @@ -85,6 +110,41 @@ private void LaunchThenStop(uint sessionId)
StopSelf();
}

/// <summary>
/// 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.
/// </summary>
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();
}
}
}

/// <summary>
/// Terminates the player process (image name "SmartThings.AVplayer.exe"). LocalSystem can
/// terminate the user's process. Best effort.
Expand Down Expand Up @@ -120,8 +180,9 @@ protected override void OnStop()
}

/// <summary>
/// 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.
/// </summary>
private void StopSelf()
{
Expand Down
15 changes: 15 additions & 0 deletions DesktopBridge/SmartThings.WAPP/Package.appxmanifest
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,21 @@
<uap5:ExecutionAlias Alias="DesktopBridgeTray.exe" />
</uap5:AppExecutionAlias>
</uap5:Extension>
<!--
Launch the tray helper at every user logon. This is what makes the tray reappear after a
Fast Startup "Shut down -> power on": the auto-start service is NOT re-run on a hybrid
resume, but logon startup tasks DO fire. Registered and removed with the package (no
orphaned system artifacts on a right-click uninstall). The tray helper is single-instance,
so this never produces a duplicate icon alongside a service-initiated launch.
-->
<desktop:Extension Category="windows.startupTask"
Executable="SmartThings.Tray\SmartThings.Tray.exe"
EntryPoint="Windows.FullTrustApplication">
<desktop:StartupTask
TaskId="DesktopBridgeTrayStartup"
Enabled="true"
DisplayName="DesktopBridge Tray" />
</desktop:Extension>
</Extensions>
</Application>

Expand Down
Loading