dragon-iso/src/TeamsISO.App/Services/TeamsLauncher.cs

144 lines
5.3 KiB
C#
Raw Normal View History

using System.Diagnostics;
using System.IO;
namespace TeamsISO.App.Services;
/// <summary>
/// Small Win32 wrapper that launches the Microsoft Teams desktop client as a
/// subprocess of TeamsISO. First step toward Phase E.1 of the embedded-Teams
/// roadmap (see docs/superpowers/specs/2026-05-08-embedded-teams-orchestration.md):
/// the operator can launch Teams from within TeamsISO so they don't have to
/// switch apps to start a meeting.
///
/// The launcher tries (in order):
/// 1. ms-teams: URI (works for both classic and new Teams)
/// 2. MSTeams.exe in %LOCALAPPDATA%\Microsoft\WindowsApps\
/// 3. Teams.exe in %LOCALAPPDATA%\Microsoft\Teams\Update.exe (legacy)
///
/// Group-routing automation (writing NDI Access Manager config so Teams
/// broadcasts on a private group) is deferred to a follow-up — for v1.0 we
/// document the manual steps in RELEASING.md and trust the operator to set
/// them once per machine.
/// </summary>
public static class TeamsLauncher
{
feat: app icon, FPS, drops counter, --version, About dialog, Stop Teams toggle Six related polish items, all building on tonight's groundwork. 1. App icon: teamsiso.ico generated from dragon-mark.png at 7 sizes (16-256), wired as ApplicationIcon in the WPF csproj, MainWindow.Icon, AboutWindow.Icon, and ARPPRODUCTICON in the WiX MSI. Taskbar / window / Add-Remove-Programs all show the dragon mark now. 2. Running incoming FPS: ring buffer of last 30 frame timestamps in IsoPipeline; ComputeFps() returns moving-average rate. Surfaced on IsoHealthStats.IncomingFps and shown in the Source column of the participants DataGrid as 'WxH · 59.94 fps'. Resets cleanly on every supervisor restart. 3. Drops counter: FrameProcessor.Stats already aggregated FramesDropped (closest-frame strategy when the receiver outpaces the processor) and FramesDuplicated; just plumbed _liveProcessor through IsoPipeline so GetStats() can read them. Exposed in the Live column under the in/out counters as a coral-tinted 'drop N'. 4. Console --version flag: prints engine version (with embedded git SHA), .NET version, OS, NDI runtime banner, expected prefix, exit-code legend, plus a wilddragon.net link. Useful for support tickets. 5. About dialog: chromeless modal with the dragon mark + version / .NET / OS / NDI runtime fields and a link to wilddragon.net. Triggered by clicking the rail logo. 6. Teams launcher Stop toggle: TeamsLauncher gains IsRunning() and StopAll(). The rail's Teams button now toggles — if Teams is up, ask to close all Teams windows via WM_CLOSE; otherwise launch as before. Confirms before stopping so we don't kill the user's call mid-transition. Tests: 74/74 unit + 9/9 NDI integration green throughout. MSI builds clean and now embeds the dragon icon for ARP.
2026-05-08 13:50:19 -04:00
/// <summary>
/// Heuristic process-name candidates we'll consider as "the Teams process" when
/// the rail toggle wants to find a running instance. New MSTeams comes first.
/// </summary>
private static readonly string[] TeamsProcessNames =
{
"ms-teams", // new MSTeams binary basename
"msteams", // alternate basename observed on some installs
"Teams", // classic Teams desktop client
};
/// <summary>
/// True if any process matching the known Teams binary basenames is running.
/// Used by the rail to decide whether to show "Launch Teams" vs "Stop Teams".
/// </summary>
public static bool IsRunning() =>
TeamsProcessNames.Any(n => Process.GetProcessesByName(n).Length > 0);
/// <summary>
/// Launches Teams. Returns true if a launch was started successfully (the
/// process may take a few seconds to actually appear). False if every
/// fallback path failed.
/// </summary>
public static bool TryLaunch(out string? errorMessage)
{
errorMessage = null;
// Path 1: URI scheme. The shell handler picks whichever Teams client
// is registered (new MSTeams.exe takes priority on modern Windows).
if (TryStart("ms-teams:", useShell: true)) return true;
// Path 2: new Teams' WindowsApps shim.
var newTeams = Path.Combine(
Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData),
"Microsoft", "WindowsApps", "ms-teams.exe");
if (File.Exists(newTeams) && TryStart(newTeams, useShell: false)) return true;
// Path 3: classic Teams Update.exe with --processStart hands off to
// the actual Teams.exe via Squirrel.
var classicUpdater = Path.Combine(
Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData),
"Microsoft", "Teams", "Update.exe");
if (File.Exists(classicUpdater))
{
try
{
Process.Start(new ProcessStartInfo
{
FileName = classicUpdater,
Arguments = "--processStart \"Teams.exe\"",
UseShellExecute = false,
CreateNoWindow = true,
});
return true;
}
catch (Exception ex)
{
errorMessage = ex.Message;
}
}
errorMessage ??= "No Microsoft Teams installation was found. Install Teams from https://www.microsoft.com/microsoft-teams and try again.";
return false;
}
feat: app icon, FPS, drops counter, --version, About dialog, Stop Teams toggle Six related polish items, all building on tonight's groundwork. 1. App icon: teamsiso.ico generated from dragon-mark.png at 7 sizes (16-256), wired as ApplicationIcon in the WPF csproj, MainWindow.Icon, AboutWindow.Icon, and ARPPRODUCTICON in the WiX MSI. Taskbar / window / Add-Remove-Programs all show the dragon mark now. 2. Running incoming FPS: ring buffer of last 30 frame timestamps in IsoPipeline; ComputeFps() returns moving-average rate. Surfaced on IsoHealthStats.IncomingFps and shown in the Source column of the participants DataGrid as 'WxH · 59.94 fps'. Resets cleanly on every supervisor restart. 3. Drops counter: FrameProcessor.Stats already aggregated FramesDropped (closest-frame strategy when the receiver outpaces the processor) and FramesDuplicated; just plumbed _liveProcessor through IsoPipeline so GetStats() can read them. Exposed in the Live column under the in/out counters as a coral-tinted 'drop N'. 4. Console --version flag: prints engine version (with embedded git SHA), .NET version, OS, NDI runtime banner, expected prefix, exit-code legend, plus a wilddragon.net link. Useful for support tickets. 5. About dialog: chromeless modal with the dragon mark + version / .NET / OS / NDI runtime fields and a link to wilddragon.net. Triggered by clicking the rail logo. 6. Teams launcher Stop toggle: TeamsLauncher gains IsRunning() and StopAll(). The rail's Teams button now toggles — if Teams is up, ask to close all Teams windows via WM_CLOSE; otherwise launch as before. Confirms before stopping so we don't kill the user's call mid-transition. Tests: 74/74 unit + 9/9 NDI integration green throughout. MSI builds clean and now embeds the dragon icon for ARP.
2026-05-08 13:50:19 -04:00
/// <summary>
/// Asks every running Teams process to close gracefully via WM_CLOSE
/// (CloseMainWindow). Returns the count of processes that exited cleanly within
/// <paramref name="gracePeriod"/>. Stragglers are NOT force-killed — Teams' own
/// "are you sure" prompt may legitimately keep a process alive briefly, and we
/// don't want to nuke the user's call mid-transition.
/// </summary>
public static int StopAll(TimeSpan? gracePeriod = null)
{
var grace = gracePeriod ?? TimeSpan.FromSeconds(3);
var deadline = DateTime.UtcNow + grace;
var asked = 0;
foreach (var name in TeamsProcessNames)
{
foreach (var p in Process.GetProcessesByName(name))
{
try
{
if (p.HasExited) { p.Dispose(); continue; }
if (p.MainWindowHandle != IntPtr.Zero)
{
p.CloseMainWindow();
asked++;
}
}
catch { /* defensive: process may have died between enumeration and signal */ }
finally { p.Dispose(); }
}
}
// Best-effort wait so the rail can flip its icon promptly.
while (DateTime.UtcNow < deadline && IsRunning())
Thread.Sleep(150);
return asked;
}
private static bool TryStart(string target, bool useShell)
{
try
{
var info = new ProcessStartInfo
{
FileName = target,
UseShellExecute = useShell,
CreateNoWindow = true,
};
Process.Start(info);
return true;
}
catch
{
return false;
}
}
}