using System.Diagnostics; using System.IO; namespace TeamsISO.App.Services; /// /// 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. /// public static class TeamsLauncher { /// /// 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. /// private static readonly string[] TeamsProcessNames = { "ms-teams", // new MSTeams binary basename "msteams", // alternate basename observed on some installs "Teams", // classic Teams desktop client }; /// /// 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". /// public static bool IsRunning() => TeamsProcessNames.Any(n => Process.GetProcessesByName(n).Length > 0); /// /// 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. /// 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; } /// /// Asks every running Teams process to close gracefully via WM_CLOSE /// (CloseMainWindow). Returns the count of processes that exited cleanly within /// . 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. /// 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; } } }