// Copyright (c) Microsoft Corporation. // Licensed under the MIT license. #include "pch.h" #include "../inc/WindowingBehavior.h" #include "Monarch.h" #include "CommandlineArgs.h" #include "FindTargetWindowArgs.h" #include "ProposeCommandlineResult.h" #include "Monarch.g.cpp" #include "../../types/inc/utils.hpp" using namespace winrt; using namespace winrt::Microsoft::Terminal; using namespace winrt::Windows::Foundation; using namespace ::Microsoft::Console; namespace winrt::Microsoft::Terminal::Remoting::implementation { Monarch::Monarch() : _ourPID{ GetCurrentProcessId() } { try { _desktopManager = winrt::create_instance(__uuidof(VirtualDesktopManager)); } CATCH_LOG(); } // This is a private constructor to be used in unit tests, where we don't // want each Monarch to necessarily use the current PID. Monarch::Monarch(const uint64_t testPID) : _ourPID{ testPID } { } Monarch::~Monarch() { } uint64_t Monarch::GetPID() { return _ourPID; } // Method Description: // - Add the given peasant to the list of peasants we're tracking. This // Peasant may have already been assigned an ID. If it hasn't, then give // it an ID. // Arguments: // - peasant: the new Peasant to track. // Return Value: // - the ID assigned to the peasant. uint64_t Monarch::AddPeasant(Remoting::IPeasant peasant) { try { // TODO:projects/5 This is terrible. There's gotta be a better way // of finding the first opening in a non-consecutive map of int->object const auto providedID = peasant.GetID(); if (providedID == 0) { // Peasant doesn't currently have an ID. Assign it a new one. peasant.AssignID(_nextPeasantID++); } else { // Peasant already had an ID (from an older monarch). Leave that one // be. Make sure that the next peasant's ID is higher than it. _nextPeasantID = providedID >= _nextPeasantID ? providedID + 1 : _nextPeasantID; } auto newPeasantsId = peasant.GetID(); // Add an event listener to the peasant's WindowActivated event. peasant.WindowActivated({ this, &Monarch::_peasantWindowActivated }); peasant.IdentifyWindowsRequested({ this, &Monarch::_identifyWindows }); peasant.RenameRequested({ this, &Monarch::_renameRequested }); _peasants[newPeasantsId] = peasant; TraceLoggingWrite(g_hRemotingProvider, "Monarch_AddPeasant", TraceLoggingUInt64(providedID, "providedID", "the provided ID for the peasant"), TraceLoggingUInt64(newPeasantsId, "peasantID", "the ID of the new peasant"), TraceLoggingLevel(WINEVENT_LEVEL_VERBOSE)); return newPeasantsId; } catch (...) { TraceLoggingWrite(g_hRemotingProvider, "Monarch_AddPeasant_Failed", TraceLoggingLevel(WINEVENT_LEVEL_VERBOSE)); // We can only get into this try/catch if the peasant died on us. So // the return value doesn't _really_ matter. They're not about to // get it. return -1; } } // Method Description: // - Event handler for the Peasant::WindowActivated event. Used as an // opportunity for us to update our internal stack of the "most recent // window". // Arguments: // - sender: the Peasant that raised this event. This might be out-of-proc! // - args: a bundle of the peasant ID, timestamp, and desktop ID, for the activated peasant // Return Value: // - void Monarch::_peasantWindowActivated(const winrt::Windows::Foundation::IInspectable& /*sender*/, const Remoting::WindowActivatedArgs& args) { HandleActivatePeasant(args); } // Method Description: // - Lookup a peasant by its ID. If the peasant has died, this will also // remove the peasant from our list of peasants. // Arguments: // - peasantID: The ID Of the peasant to find // Return Value: // - the peasant if it exists in our map, otherwise null Remoting::IPeasant Monarch::_getPeasant(uint64_t peasantID) { try { const auto peasantSearch = _peasants.find(peasantID); auto maybeThePeasant = peasantSearch == _peasants.end() ? nullptr : peasantSearch->second; // Ask the peasant for their PID. This will validate that they're // actually still alive. if (maybeThePeasant) { maybeThePeasant.GetPID(); } return maybeThePeasant; } catch (...) { LOG_CAUGHT_EXCEPTION(); // Remove the peasant from the list of peasants _peasants.erase(peasantID); // Remove the peasant from the list of MRU windows. They're dead. // They can't be the MRU anymore. _clearOldMruEntries(peasantID); return nullptr; } } // Method Description: // - Find the ID of the peasant with the given name. If no such peasant // exists, then we'll return 0. If we encounter any peasants who have died // during this process, then we'll remove them from the set of _peasants // Arguments: // - name: The window name to look for // Return Value: // - 0 if we didn't find the given peasant, otherwise a positive number for // the window's ID. uint64_t Monarch::_lookupPeasantIdForName(std::wstring_view name) { if (name.empty()) { return 0; } std::vector peasantsToErase{}; uint64_t result = 0; for (const auto& [id, p] : _peasants) { try { auto otherName = p.WindowName(); if (otherName == name) { result = id; break; } } catch (...) { LOG_CAUGHT_EXCEPTION(); // Normally, we'd just erase the peasant here. However, we can't // erase from the map while we're iterating over it like this. // Instead, pull a good ole Java and collect this id for removal // later. peasantsToErase.push_back(id); } } // Remove the dead peasants we came across while iterating. for (const auto& id : peasantsToErase) { // Remove the peasant from the list of peasants _peasants.erase(id); // Remove the peasant from the list of MRU windows. They're dead. // They can't be the MRU anymore. _clearOldMruEntries(id); } return result; } // Method Description: // - Handler for the `Peasant::WindowActivated` event. We'll make a in-proc // copy of the WindowActivatedArgs from the peasant. That way, we won't // need to worry about the origin process dying when working with the // WindowActivatedArgs. // - If the peasant process dies while we're making this copy, then we'll // just log it and do nothing. We certainly don't want to track a dead // peasant. // - We'll pass that copy of the WindowActivatedArgs to // _doHandleActivatePeasant, which will actually insert the // WindowActivatedArgs into the list we're using to track the most recent // peasants. // Arguments: // - args: the WindowActivatedArgs describing when and where the peasant was activated. // Return Value: // - void Monarch::HandleActivatePeasant(const Remoting::WindowActivatedArgs& args) { // Start by making a local copy of these args. It's easier for us if our // tracking of these args is all in-proc. That way, the only thing that // could fail due to the peasant dying is _this first copy_. winrt::com_ptr localArgs{ nullptr }; try { localArgs = winrt::make_self(args); // This method will actually do the hard work _doHandleActivatePeasant(localArgs); } catch (...) { TraceLoggingWrite(g_hRemotingProvider, "Monarch_HandleActivatePeasant_Failed", TraceLoggingLevel(WINEVENT_LEVEL_VERBOSE)); } } // Method Description: // - Helper for removing a peasant from the list of MRU peasants. We want to // do this both when the peasant dies, and also when the peasant is newly // activated (so that we don't leave an old entry for it in the list). // Arguments: // - peasantID: The ID of the peasant to remove from the MRU list // Return Value: // - void Monarch::_clearOldMruEntries(const uint64_t peasantID) { auto result = std::find_if(_mruPeasants.begin(), _mruPeasants.end(), [peasantID](auto&& other) { return peasantID == other.PeasantID(); }); if (result != std::end(_mruPeasants)) { TraceLoggingWrite(g_hRemotingProvider, "Monarch_RemovedPeasantFromDesktop", TraceLoggingUInt64(peasantID, "peasantID", "The ID of the peasant"), TraceLoggingGuid(result->DesktopID(), "desktopGuid", "The GUID of the previous desktop the window was on"), TraceLoggingLevel(WINEVENT_LEVEL_VERBOSE)); _mruPeasants.erase(result); } } // Method Description: // - Actually handle inserting the WindowActivatedArgs into our list of MRU windows. // Arguments: // - localArgs: an in-proc WindowActivatedArgs that we should add to our list of MRU windows. // Return Value: // - void Monarch::_doHandleActivatePeasant(const winrt::com_ptr& localArgs) { const auto newLastActiveTime = localArgs->ActivatedTime().time_since_epoch().count(); // * Check all the current lists to look for this peasant. // remove it from any where it exists. _clearOldMruEntries(localArgs->PeasantID()); // * If the current desktop doesn't have a vector, add one. const auto desktopGuid{ localArgs->DesktopID() }; // * Add this args list. By using lower_bound with insert, we can get it // into exactly the right spot, without having to re-sort the whole // array. _mruPeasants.insert(std::lower_bound(_mruPeasants.begin(), _mruPeasants.end(), *localArgs, [](const auto& first, const auto& second) { return first.ActivatedTime() > second.ActivatedTime(); }), *localArgs); TraceLoggingWrite(g_hRemotingProvider, "Monarch_SetMostRecentPeasant", TraceLoggingUInt64(localArgs->PeasantID(), "peasantID", "the ID of the activated peasant"), TraceLoggingGuid(desktopGuid, "desktopGuid", "The GUID of the desktop the window is on"), TraceLoggingInt64(newLastActiveTime, "newLastActiveTime", "The provided localArgs->ActivatedTime()"), TraceLoggingLevel(WINEVENT_LEVEL_VERBOSE)); } // Method Description: // - Retrieves the ID of the MRU peasant window. If requested, will limit // the search to windows that are on the current desktop. // Arguments: // - limitToCurrentDesktop: if true, only return the MRU peasant that's // actually on the current desktop. // Return Value: // - the ID of the most recent peasant, otherwise 0 if we could not find one. uint64_t Monarch::_getMostRecentPeasantID(const bool limitToCurrentDesktop) { if (_mruPeasants.empty()) { // We haven't yet been told the MRU peasant. Just use the first one. // This is just gonna be a random one, but really shouldn't happen // in practice. The WindowManager should set the MRU peasant // immediately as soon as it creates the monarch/peasant for the // first window. if (_peasants.size() > 0) { try { return _peasants.begin()->second.GetID(); } catch (...) { // This shouldn't really happen. If we're the monarch, then the // first peasant should also _be us_. So we should be able to // get our own ID. return 0; } } TraceLoggingWrite(g_hRemotingProvider, "Monarch_getMostRecentPeasantID_NoPeasants", TraceLoggingLevel(WINEVENT_LEVEL_VERBOSE)); return 0; } // Here, there's at least one MRU peasant. // // We're going to iterate over these peasants until we find one that both: // 1. Is alive // 2. Meets our selection criteria (do we care if it is on this desktop?) // // If the peasant is dead, then we'll remove it, and try the next one. // Once we find one that's alive, we'll either: // * check if we only want a peasant on the current desktop, and if so, // check if this peasant is on the current desktop. // - If it isn't on the current desktop, we'll loop again, on the // following peasant. // * If we don't care, then we'll just return that one. // // We're not just using an iterator because the contents of the list // might change while we're iterating here (if the peasant is dead we'll // remove it from the list). int positionInList = 0; while (_mruPeasants.cbegin() + positionInList < _mruPeasants.cend()) { const auto mruWindowArgs{ *(_mruPeasants.begin() + positionInList) }; const auto peasant{ _getPeasant(mruWindowArgs.PeasantID()) }; if (!peasant) { TraceLoggingWrite(g_hRemotingProvider, "Monarch_Collect_WasDead", TraceLoggingUInt64(mruWindowArgs.PeasantID(), "peasantID", "We thought this peasant was the MRU one, but it was actually already dead."), TraceLoggingGuid(mruWindowArgs.DesktopID(), "desktopGuid", "The GUID of the desktop the window is on"), TraceLoggingLevel(WINEVENT_LEVEL_VERBOSE)); // We'll go through the loop again. We removed the current one // at positionInList, so the next one in positionInList will be // a new, different peasant. continue; } if (limitToCurrentDesktop && _desktopManager) { // Check if this peasant is actually on this desktop. We can't // simply get the GUID of the current desktop. We have to ask if // the HWND is on the current desktop. BOOL onCurrentDesktop{ false }; // SUCCEEDED_LOG will log if it failed, and return true if it SUCCEEDED if (SUCCEEDED_LOG(_desktopManager->IsWindowOnCurrentVirtualDesktop(reinterpret_cast(mruWindowArgs.Hwnd()), &onCurrentDesktop)) && onCurrentDesktop) { TraceLoggingWrite(g_hRemotingProvider, "Monarch_Collect", TraceLoggingUInt64(mruWindowArgs.PeasantID(), "peasantID", "the ID of the MRU peasant for a desktop"), TraceLoggingGuid(mruWindowArgs.DesktopID(), "desktopGuid", "The GUID of the desktop the window is on"), TraceLoggingBoolean(limitToCurrentDesktop, "limitToCurrentDesktop", "True if we should only search for a window on the current desktop"), TraceLoggingBool(onCurrentDesktop, "onCurrentDesktop", "true if this window was in fact on the current desktop"), TraceLoggingLevel(WINEVENT_LEVEL_VERBOSE)); return mruWindowArgs.PeasantID(); } // If this window wasn't on the current desktop, another one // might be. We'll increment positionInList below, and try // again. } else { TraceLoggingWrite(g_hRemotingProvider, "Monarch_getMostRecentPeasantID_Found", TraceLoggingUInt64(mruWindowArgs.PeasantID(), "peasantID", "The ID of the MRU peasant"), TraceLoggingLevel(WINEVENT_LEVEL_VERBOSE)); return mruWindowArgs.PeasantID(); } positionInList++; } // Here, we've checked all the windows, and none of them was both alive // and the most recent (on this desktop). Just return 0 - the caller // will use this to create a new window. TraceLoggingWrite(g_hRemotingProvider, "Monarch_getMostRecentPeasantID_NotFound", TraceLoggingLevel(WINEVENT_LEVEL_VERBOSE)); return 0; } // Method Description: // - Try to handle a commandline from a new WT invocation. We might need to // hand the commandline to an existing window, or we might need to tell // the caller that they need to become a new window to handle it themselves. // Arguments: // - // Return Value: // - true if the caller should create a new window for this commandline. // False otherwise - the monarch should have dispatched this commandline // to another window in this case. Remoting::ProposeCommandlineResult Monarch::ProposeCommandline(const Remoting::CommandlineArgs& args) { // Raise an event, to ask how to handle this commandline. We can't ask // the app ourselves - we exist isolated from that knowledge (and // dependency hell). The WindowManager will raise this up to the app // host, which will then ask the AppLogic, who will then parse the // commandline and determine the provided ID of the window. auto findWindowArgs{ winrt::make_self(args) }; // This is handled by some handler in-proc _FindTargetWindowRequestedHandlers(*this, *findWindowArgs); // After the event was handled, ResultTargetWindow() will be filled with // the parsed result. const auto targetWindow = findWindowArgs->ResultTargetWindow(); const auto targetWindowName = findWindowArgs->ResultTargetWindowName(); TraceLoggingWrite(g_hRemotingProvider, "Monarch_ProposeCommandline", TraceLoggingInt64(targetWindow, "targetWindow", "The window ID the args specified"), TraceLoggingLevel(WINEVENT_LEVEL_VERBOSE)); // If there's a valid ID returned, then let's try and find the peasant // that goes with it. Alternatively, if we were given a magic windowing // constant, we can use that to look up an appropriate peasant. if (targetWindow >= 0 || targetWindow == WindowingBehaviorUseName || targetWindow == WindowingBehaviorUseExisting || targetWindow == WindowingBehaviorUseAnyExisting) { uint64_t windowID = 0; switch (targetWindow) { case WindowingBehaviorUseCurrent: case WindowingBehaviorUseExisting: // TODO:projects/5 for now, just use the MRU window. Technically, // UseExisting and UseCurrent are different. // UseCurrent implies that we should try to do the WT_SESSION // lookup to find the window that spawned this process (then // fall back to sameDesktop if we can't find a match). For now, // it's good enough to just try to find a match on this desktop. windowID = _getMostRecentPeasantID(true); break; case WindowingBehaviorUseAnyExisting: windowID = _getMostRecentPeasantID(false); break; case WindowingBehaviorUseName: windowID = _lookupPeasantIdForName(targetWindowName); break; default: windowID = ::base::saturated_cast(targetWindow); break; } TraceLoggingWrite(g_hRemotingProvider, "Monarch_ProposeCommandline", TraceLoggingInt64(windowID, "windowID", "The actual peasant ID we evaluated the window ID as"), TraceLoggingLevel(WINEVENT_LEVEL_VERBOSE)); // If_getMostRecentPeasantID returns 0 above, then we couldn't find // a matching window for that style of windowing. _getPeasant will // return nullptr, and we'll fall through to the "create a new // window" branch below. if (auto targetPeasant{ _getPeasant(windowID) }) { auto result{ winrt::make_self(false) }; try { // This will raise the peasant's ExecuteCommandlineRequested // event, which will then ask the AppHost to handle the // commandline, which will then pass it to AppLogic for // handling. targetPeasant.ExecuteCommandline(args); } catch (...) { // If we fail to propose the commandline to the peasant (it // died?) then just tell this process to become a new window // instead. result->WindowName(targetWindowName); result->ShouldCreateWindow(true); // If this fails, it'll be logged in the following // TraceLoggingWrite statement, with succeeded=false } TraceLoggingWrite(g_hRemotingProvider, "Monarch_ProposeCommandline_Existing", TraceLoggingUInt64(windowID, "peasantID", "the ID of the peasant the commandline waws intended for"), TraceLoggingBoolean(true, "foundMatch", "true if we found a peasant with that ID"), TraceLoggingBoolean(!result->ShouldCreateWindow(), "succeeded", "true if we successfully dispatched the commandline to the peasant"), TraceLoggingLevel(WINEVENT_LEVEL_VERBOSE)); return *result; } else if (windowID > 0) { // In this case, an ID was provided, but there's no // peasant with that ID. Instead, we should tell the caller that // they should make a new window, but _with that ID_. TraceLoggingWrite(g_hRemotingProvider, "Monarch_ProposeCommandline_Existing", TraceLoggingUInt64(windowID, "peasantID", "the ID of the peasant the commandline waws intended for"), TraceLoggingBoolean(false, "foundMatch", "true if we found a peasant with that ID"), TraceLoggingLevel(WINEVENT_LEVEL_VERBOSE)); auto result{ winrt::make_self(true) }; result->Id(windowID); result->WindowName(targetWindowName); return *result; } } // If we get here, we couldn't find an existing window. Make a new one. TraceLoggingWrite(g_hRemotingProvider, "Monarch_ProposeCommandline_NewWindow", TraceLoggingInt64(targetWindow, "targetWindow", "The provided ID"), TraceLoggingLevel(WINEVENT_LEVEL_VERBOSE)); // In this case, no usable ID was provided. Return { true, nullopt } auto result = winrt::make_self(true); result->WindowName(targetWindowName); return *result; } // Method Description: // - Helper for doing something on each and every peasant, with no regard // for if the peasant is living or dead. // - We'll try calling callback on every peasant. // - If any single peasant is dead, then we'll call errorCallback, and move on. // - We're taking an errorCallback here, because the thing we usually want // to do is TraceLog a message, but TraceLoggingWrite is actually a macro // that _requires_ the second arg to be a string literal. It can't just be // a variable. // Arguments: // - callback: The function to call on each peasant // - errorCallback: The function to call if a peasant is dead. // Return Value: // - void Monarch::_forAllPeasantsIgnoringTheDead(std::function callback, std::function errorCallback) { for (const auto& [id, p] : _peasants) { try { callback(p, id); } catch (...) { LOG_CAUGHT_EXCEPTION(); // If this fails, we don't _really_ care. Just move on to the // next one. Someone else will clean up the dead peasant. errorCallback(id); } } } // Method Description: // - This is an event handler for the IdentifyWindowsRequested event. A // Peasant may raise that event if they want _all_ windows to identify // themselves. // - This will tell each and every peasant to identify themselves. This will // eventually propagate down to TerminalPage::IdentifyWindow. // Arguments: // - // Return Value: // - void Monarch::_identifyWindows(const winrt::Windows::Foundation::IInspectable& /*sender*/, const winrt::Windows::Foundation::IInspectable& /*args*/) { // Notify all the peasants to display their ID. auto callback = [](auto&& p, auto&& /*id*/) { p.DisplayWindowId(); }; auto onError = [](auto&& id) { TraceLoggingWrite(g_hRemotingProvider, "Monarch_identifyWindows_Failed", TraceLoggingInt64(id, "peasantID", "The ID of the peasant which we could not identify"), TraceLoggingLevel(WINEVENT_LEVEL_VERBOSE)); }; _forAllPeasantsIgnoringTheDead(callback, onError); } // Method Description: // - This is an event handler for the RenameRequested event. A // Peasant may raise that event when they want to be renamed to something else. // - We will check if there are any other windows with this name. If there // are, then we'll reject the rename by setting args.Succeeded=false. // - If there aren't any other windows with this name, then we'll set // args.Succeeded=true, allowing the window to keep this name. // Arguments: // - args: Contains the requested window name and a boolean (Succeeded) // indicating if the request was successful. // Return Value: // - void Monarch::_renameRequested(const winrt::Windows::Foundation::IInspectable& /*sender*/, const winrt::Microsoft::Terminal::Remoting::RenameRequestArgs& args) { bool successfullyRenamed = false; try { args.Succeeded(false); const auto name{ args.NewName() }; // Try to find a peasant that currently has this name const auto id = _lookupPeasantIdForName(name); if (_getPeasant(id) == nullptr) { // If there is one, then oh no! The requestor is not allowed to // be renamed. args.Succeeded(true); successfullyRenamed = true; } TraceLoggingWrite(g_hRemotingProvider, "Monarch_renameRequested", TraceLoggingWideString(name.c_str(), "name", "The newly proposed name"), TraceLoggingInt64(successfullyRenamed, "successfullyRenamed", "true if the peasant is allowed to rename themselves to that name."), TraceLoggingLevel(WINEVENT_LEVEL_VERBOSE)); } catch (...) { LOG_CAUGHT_EXCEPTION(); // If this fails, we don't _really_ care. The peasant died, but // they're the only one who cares about the result. TraceLoggingWrite(g_hRemotingProvider, "Monarch_renameRequested_Failed", TraceLoggingLevel(WINEVENT_LEVEL_VERBOSE)); } } }