Add a Monarch/Peasant sample app (#8171)

This PR adds a sample monarch/peasant application. This is a type of
application where a single "Monarch" can coordinate the actions of multiple
other "Peasant" processes, as described by the specs in #7240 and #8135.

This project is intended to be a standalone sample of how the architecture would
work, without involving the entirety of the Windows Terminal build. Eventually,
this architecture will be incorporated into `wt.exe` itself, to enable scenarios
like:
* Run `wt` in the current window (#4472)
* Single Instance Mode (#2227)

For an example of this sample running, see the below GIF:

![monarch-peasant-sample-001](https://user-images.githubusercontent.com/18356694/98262202-f39b1500-1f4a-11eb-9220-4af4d922339f.gif)

This sample operates largely by printing to the console, to help the reader
understand how it's working through its logic.

I'm doing this mostly so we can have a _committed_ sample of this type of application, kinda like how VtPipeTerm is a sample ConPTY application. It's a lot easier to understand (& build on) when there aren't any window shenanigans, settings loading, Island instantiation, or anything else that the whole of `WindowsTerminal.exe` needs

* [x] I work here
* [x] This is sample code, so I'm not shipping tests for it.
* [x] Go see the doc over in #8135
This commit is contained in:
Mike Griese 2021-01-19 15:55:30 -06:00 committed by GitHub
parent a7d7362b95
commit c33a97955f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
30 changed files with 1556 additions and 2 deletions

View File

@ -40,6 +40,7 @@ ITab
ITaskbar
LCID
llabs
llu
localtime
lround
LSHIFT
@ -61,6 +62,7 @@ PAGESCROLL
REGCLS
pmr
RETURNCMD
REGCLS
rfind
roundf
RSHIFT

File diff suppressed because it is too large Load Diff

View File

@ -21,7 +21,8 @@
"/res/terminal/",
"/doc/specs/",
"/doc/cascadia/",
"/doc/user-docs/"
"/doc/user-docs/",
"/src/tools/MonarchPeasantSample/",
],
"SuffixFilters": [
".dbb",
@ -38,5 +39,5 @@
".rec",
".err",
".xlsx"
]
]
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.1 KiB

View File

@ -0,0 +1,88 @@
<?xml version="1.0" encoding="utf-8"?>
<Project ToolsVersion="15.0" DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<PropertyGroup Condition="'$(VisualStudioVersion)' == '' or '$(VisualStudioVersion)' &lt; '15.0'">
<VisualStudioVersion>15.0</VisualStudioVersion>
</PropertyGroup>
<ItemGroup Label="ProjectConfigurations">
<ProjectConfiguration Include="Debug|x86">
<Configuration>Debug</Configuration>
<Platform>x86</Platform>
</ProjectConfiguration>
<ProjectConfiguration Include="Release|x86">
<Configuration>Release</Configuration>
<Platform>x86</Platform>
</ProjectConfiguration>
<ProjectConfiguration Include="Debug|x64">
<Configuration>Debug</Configuration>
<Platform>x64</Platform>
</ProjectConfiguration>
<ProjectConfiguration Include="Release|x64">
<Configuration>Release</Configuration>
<Platform>x64</Platform>
</ProjectConfiguration>
<ProjectConfiguration Include="Debug|ARM">
<Configuration>Debug</Configuration>
<Platform>ARM</Platform>
</ProjectConfiguration>
<ProjectConfiguration Include="Release|ARM">
<Configuration>Release</Configuration>
<Platform>ARM</Platform>
</ProjectConfiguration>
<ProjectConfiguration Include="Debug|ARM64">
<Configuration>Debug</Configuration>
<Platform>ARM64</Platform>
</ProjectConfiguration>
<ProjectConfiguration Include="Release|ARM64">
<Configuration>Release</Configuration>
<Platform>ARM64</Platform>
</ProjectConfiguration>
<ProjectConfiguration Include="Debug|AnyCPU">
<Configuration>Debug</Configuration>
<Platform>AnyCPU</Platform>
</ProjectConfiguration>
<ProjectConfiguration Include="Release|AnyCPU">
<Configuration>Release</Configuration>
<Platform>AnyCPU</Platform>
</ProjectConfiguration>
</ItemGroup>
<PropertyGroup>
<WapProjPath Condition="'$(WapProjPath)'==''">$(MSBuildExtensionsPath)\Microsoft\DesktopBridge\</WapProjPath>
</PropertyGroup>
<Import Project="$(WapProjPath)\Microsoft.DesktopBridge.props" />
<PropertyGroup>
<ProjectGuid>f75e29d0-d288-478b-8d83-2c190f321a3f</ProjectGuid>
<TargetPlatformVersion>10.0.18362.0</TargetPlatformVersion>
<TargetPlatformMinVersion>10.0.17763.0</TargetPlatformMinVersion>
<DefaultLanguage>en-US</DefaultLanguage>
<AppxPackageSigningEnabled>false</AppxPackageSigningEnabled>
<EntryPointProjectUniqueName>..\MonarchPeasantSample\MonarchPeasantSample.vcxproj</EntryPointProjectUniqueName>
</PropertyGroup>
<ItemGroup>
<AppxManifest Include="Package.appxmanifest">
<SubType>Designer</SubType>
</AppxManifest>
</ItemGroup>
<ItemGroup>
<Content Include="Images\SplashScreen.scale-200.png" />
<Content Include="Images\LockScreenLogo.scale-200.png" />
<Content Include="Images\Square150x150Logo.scale-200.png" />
<Content Include="Images\Square44x44Logo.scale-200.png" />
<Content Include="Images\Square44x44Logo.targetsize-24_altform-unplated.png" />
<Content Include="Images\StoreLogo.png" />
<Content Include="Images\Wide310x150Logo.scale-200.png" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\MonarchPeasantSample\MonarchPeasantSample.vcxproj" >
<!--
THESE PROPERTIES ARE LOAD BEARING!
We need them so the MonarchPeasantSample.winmd will be placed in the
package root. If it's not there, then we won't be able to activate our
WinRT classes!
-->
<Private>true</Private>
<CopyLocalSatelliteAssemblies>true</CopyLocalSatelliteAssemblies>
</ProjectReference>
</ItemGroup>
<Import Project="$(WapProjPath)\Microsoft.DesktopBridge.targets" />
</Project>

View File

@ -0,0 +1,62 @@
<?xml version="1.0" encoding="utf-8"?>
<Package
xmlns="http://schemas.microsoft.com/appx/manifest/foundation/windows10"
xmlns:uap="http://schemas.microsoft.com/appx/manifest/uap/windows10"
xmlns:rescap="http://schemas.microsoft.com/appx/manifest/foundation/windows10/restrictedcapabilities"
xmlns:uap3="http://schemas.microsoft.com/appx/manifest/uap/windows10/3"
xmlns:uap5="http://schemas.microsoft.com/appx/manifest/uap/windows10/5"
xmlns:desktop="http://schemas.microsoft.com/appx/manifest/desktop/windows10"
IgnorableNamespaces="uap rescap">
<Identity
Name="5d4b020d-14a7-4a1a-a359-3f468e23bae3"
Publisher="CN=migrie"
Version="1.0.0.0" />
<Properties>
<DisplayName>MonarchPeasantPackage</DisplayName>
<PublisherDisplayName>migrie</PublisherDisplayName>
<Logo>Images\StoreLogo.png</Logo>
</Properties>
<Dependencies>
<TargetDeviceFamily Name="Windows.Universal" MinVersion="10.0.0.0" MaxVersionTested="10.0.0.0" />
<TargetDeviceFamily Name="Windows.Desktop" MinVersion="10.0.14393.0" MaxVersionTested="10.0.14393.0" />
</Dependencies>
<Resources>
<Resource Language="x-generate"/>
</Resources>
<Applications>
<Application Id="App"
Executable="$targetnametoken$.exe"
EntryPoint="$targetentrypoint$">
<uap:VisualElements
DisplayName="MonarchPeasantPackage"
Description="MonarchPeasantPackage"
BackgroundColor="transparent"
Square150x150Logo="Images\Square150x150Logo.png"
Square44x44Logo="Images\Square44x44Logo.png">
<uap:DefaultTile Wide310x150Logo="Images\Wide310x150Logo.png" />
<uap:SplashScreen Image="Images\SplashScreen.png" />
</uap:VisualElements>
<Extensions>
<uap3:Extension Category="windows.appExecutionAlias" Executable="MonarchPeasantSample\MonarchPeasantSample.exe" EntryPoint="Windows.FullTrustApplication">
<uap3:AppExecutionAlias>
<desktop:ExecutionAlias Alias="MonarchPeasantSample.exe" />
</uap3:AppExecutionAlias>
</uap3:Extension>
</Extensions>
</Application>
</Applications>
<Capabilities>
<Capability Name="internetClient" />
<rescap:Capability Name="runFullTrust" />
</Capabilities>
</Package>

View File

@ -0,0 +1,119 @@
#include "pch.h"
#include "AppState.h"
#include "../../types/inc/utils.hpp"
using namespace winrt;
using namespace winrt::Windows::Foundation;
using namespace ::Microsoft::Console;
void AppState::_setupConsole()
{
hOutput = GetStdHandle(STD_OUTPUT_HANDLE);
hInput = GetStdHandle(STD_INPUT_HANDLE);
DWORD dwMode = 0;
GetConsoleMode(hOutput, &dwMode);
dwMode |= ENABLE_VIRTUAL_TERMINAL_PROCESSING;
SetConsoleMode(hOutput, dwMode);
}
void AppState::initializeState()
{
// Initialize the console handles
_setupConsole();
// Set up WinRT
init_apartment();
}
bool AppState::areWeTheKing(const bool logPIDs)
{
auto kingPID = monarch.GetPID();
auto ourPID = GetCurrentProcessId();
if (logPIDs)
{
if (ourPID == kingPID)
{
printf(fmt::format("We're the\x1b[33m king\x1b[m - our PID is {}\n", ourPID).c_str());
}
else
{
printf(fmt::format("We're a lowly peasant - the king is {}\n", kingPID).c_str());
}
}
return (ourPID == kingPID);
}
void AppState::remindKingWhoTheyAre(const winrt::MonarchPeasantSample::IPeasant& iPeasant)
{
winrt::com_ptr<MonarchPeasantSample::implementation::Monarch> monarchImpl;
monarchImpl.copy_from(winrt::get_self<MonarchPeasantSample::implementation::Monarch>(monarch));
if (monarchImpl)
{
auto ourID = iPeasant.GetID();
monarchImpl->SetSelfID(ourID);
monarchImpl->AddPeasant(iPeasant);
printf("The king is peasant #%lld\n", ourID);
}
else
{
printf("Shoot, we wanted to be able to get the monarchImpl here but couldnt\n");
}
}
winrt::MonarchPeasantSample::Monarch AppState::instantiateMonarch()
{
// Heads up! This only works because we're using
// "metadata-based-marshalling" for our WinRT types. THat means the OS is
// using the .winmd file we generate to figure out the proxy/stub
// definitions for our types automatically. This only works in the following
// cases:
//
// * If we're running unpackaged: the .winmd but be a sibling of the .exe
// * If we're running packaged: the .winmd must be in the package root
auto monarch = create_instance<winrt::MonarchPeasantSample::Monarch>(Monarch_clsid,
CLSCTX_LOCAL_SERVER);
return monarch;
}
MonarchPeasantSample::IPeasant AppState::_createOurPeasant()
{
auto peasant = winrt::make_self<MonarchPeasantSample::implementation::Peasant>();
auto ourID = monarch.AddPeasant(*peasant);
printf("The monarch assigned us the ID %llu\n", ourID);
if (areWeTheKing())
{
remindKingWhoTheyAre(*peasant);
}
return *peasant;
}
void AppState::createMonarch()
{
monarch = AppState::instantiateMonarch();
}
// return true to exit early, false if we should continue into the main loop
bool AppState::processCommandline()
{
const bool isKing = areWeTheKing(false);
// If we're the king, we _definitely_ want to process the arguments, we were
// launched with them!
//
// Otherwise, the King will tell us if we should make a new window
const bool createNewWindow = isKing || monarch.ProposeCommandline({ args }, { L"placeholder CWD" });
if (createNewWindow)
{
peasant = _createOurPeasant();
peasant.ExecuteCommandline({ args }, { L"placeholder CWD" });
return false;
}
else
{
printf("The Monarch instructed us to not create a new window. We'll be exiting now.\n");
}
return true;
}

View File

@ -0,0 +1,32 @@
#pragma once
#include "SampleMonarch.h"
#include "SamplePeasant.h"
#include "../../types/inc/utils.hpp"
class AppState
{
public:
bool areWeTheKing(const bool logPIDs = false);
void initializeState();
static winrt::MonarchPeasantSample::Monarch instantiateMonarch();
void createMonarch();
bool processCommandline();
void remindKingWhoTheyAre(const winrt::MonarchPeasantSample::IPeasant& peasant);
HANDLE hInput{ INVALID_HANDLE_VALUE };
HANDLE hOutput{ INVALID_HANDLE_VALUE };
winrt::MonarchPeasantSample::IPeasant peasant{ nullptr };
winrt::MonarchPeasantSample::Monarch monarch{ nullptr };
std::vector<winrt::hstring> args;
private:
void _setupConsole();
int _appLoop();
winrt::MonarchPeasantSample::IPeasant _createOurPeasant();
};
bool monarchAppLoop(AppState& state); // Defined in MonarchMain.cpp
bool peasantAppLoop(AppState& state); // Defined in PeasantMain.cpp

View File

@ -0,0 +1,38 @@
#include "pch.h"
#include "AppState.h"
#include "../../types/inc/utils.hpp"
void printPeasants(const winrt::MonarchPeasantSample::Monarch& /*monarch*/)
{
printf("This is unimplemented\n");
}
bool monarchAppLoop(AppState& state)
{
bool exitRequested = false;
printf("Press `l` to list peasants, 'm' to change modes `q` to quit\n");
winrt::com_ptr<winrt::MonarchPeasantSample::implementation::Monarch> monarchImpl;
monarchImpl.copy_from(winrt::get_self<winrt::MonarchPeasantSample::implementation::Monarch>(state.monarch));
while (!exitRequested)
{
const auto ch = _getch();
if (ch == 'l')
{
printPeasants(state.monarch);
}
else if (ch == 'q')
{
exitRequested = true;
}
else if (ch == 'm')
{
if (monarchImpl)
{
monarchImpl->ToggleWindowingBehavior();
}
}
}
return true;
}

View File

@ -0,0 +1,91 @@
<?xml version="1.0" encoding="utf-8"?>
<Project DefaultTargets="Build" ToolsVersion="15.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<PropertyGroup Label="Globals">
<ProjectGuid>{21b7ea5e-1ef8-49b6-ac07-11714af0e37d}</ProjectGuid>
<Keyword>Win32Proj</Keyword>
<RootNamespace>MonarchPeasantSample</RootNamespace>
<ProjectName>MonarchPeasantSample</ProjectName>
<TargetName>MonarchPeasantSample</TargetName>
<ConfigurationType>Application</ConfigurationType>
<OpenConsoleUniversalApp>false</OpenConsoleUniversalApp>
<ApplicationType>Windows Store</ApplicationType>
<TargetPlatformIdentifier>Windows</TargetPlatformIdentifier>
</PropertyGroup>
<Import Project="..\..\..\common.openconsole.props" Condition="'$(OpenConsoleDir)'==''" />
<Import Project="$(OpenConsoleDir)src\cppwinrt.build.pre.props" />
<PropertyGroup>
<GenerateManifest>true</GenerateManifest>
<EmbedManifest>true</EmbedManifest>
</PropertyGroup>
<!-- Source Files -->
<ItemGroup>
<ClInclude Include="pch.h" />
<ClInclude Include="AppState.h" />
<ClInclude Include="SampleMonarch.h">
<DependentUpon>SampleMonarch.idl</DependentUpon>
</ClInclude>
<ClInclude Include="SamplePeasant.h">
<DependentUpon>SamplePeasant.idl</DependentUpon>
</ClInclude>
</ItemGroup>
<ItemGroup>
<ClCompile Include="main.cpp" />
<ClCompile Include="AppState.cpp" />
<ClCompile Include="MonarchMain.cpp" />
<ClCompile Include="PeasantMain.cpp" />
<ClCompile Include="pch.cpp">
<PrecompiledHeader>Create</PrecompiledHeader>
</ClCompile>
<ClCompile Include="SampleMonarch.cpp">
<DependentUpon>SampleMonarch.idl</DependentUpon>
</ClCompile>
<ClCompile Include="SamplePeasant.cpp">
<DependentUpon>SamplePeasant.idl</DependentUpon>
</ClCompile>
</ItemGroup>
<ItemGroup>
<Midl Include="SampleMonarch.idl" />
<Midl Include="SamplePeasant.idl" />
</ItemGroup>
<ItemGroup>
<None Include="packages.config" />
</ItemGroup>
<!-- Dependencies -->
<ItemGroup>
<ProjectReference Include="$(OpenConsoleDir)src\types\lib\types.vcxproj" />
</ItemGroup>
<!--
This ItemGroup and the Globals PropertyGroup below it are required in order
to enable F5 debugging for the unpackaged application
-->
<ItemGroup>
<PropertyPageSchema Include="$(VCTargetsPath)$(LangID)\debugger_general.xml" />
<PropertyPageSchema Include="$(VCTargetsPath)$(LangID)\debugger_local_windows.xml" />
</ItemGroup>
<PropertyGroup Label="Globals">
<DebuggerFlavor>WindowsLocalDebugger</DebuggerFlavor>
</PropertyGroup>
<Import Project="$(OpenConsoleDir)src\cppwinrt.build.post.props" />
<!-- These have to come after post.props because the Cpp common targets will inexplicably overwrite them. -->
<ItemDefinitionGroup>
<ClCompile>
<SDLCheck>true</SDLCheck>
</ClCompile>
<Link>
<SubSystem>Console</SubSystem>
</Link>
</ItemDefinitionGroup>
<Import Project="$(OpenConsoleDir)\build\rules\GenerateSxsManifestsFromWinmds.targets" />
</Project>

View File

@ -0,0 +1,108 @@
#include "pch.h"
#include "AppState.h"
#include "../../types/inc/utils.hpp"
bool peasantReadInput(AppState& state)
{
DWORD cNumRead, i;
std::array<INPUT_RECORD, 128> irInBuf;
if (!ReadConsoleInput(state.hInput, // input buffer handle
irInBuf.data(), // buffer to read into
static_cast<DWORD>(irInBuf.size()), // size of read buffer
&cNumRead)) // number of records read
{
printf("\x1b[31mReadConsoleInput failed\x1b[m\n");
ExitProcess(0);
}
for (i = 0; i < cNumRead; i++)
{
switch (irInBuf[i].EventType)
{
case KEY_EVENT: // keyboard input
{
auto key = irInBuf[i].Event.KeyEvent;
if (key.bKeyDown == false &&
key.uChar.UnicodeChar == L'q')
{
return true;
}
else
{
printf("This window was activated\n");
winrt::com_ptr<winrt::MonarchPeasantSample::implementation::Peasant> peasantImpl;
peasantImpl.copy_from(winrt::get_self<winrt::MonarchPeasantSample::implementation::Peasant>(state.peasant));
if (peasantImpl)
{
peasantImpl->raiseActivatedEvent();
}
}
break;
}
case MOUSE_EVENT:
case WINDOW_BUFFER_SIZE_EVENT:
case FOCUS_EVENT:
case MENU_EVENT:
break;
default:
printf("\x1b[33mUnknown event from ReadConsoleInput - this is probably impossible\x1b[m\n");
ExitProcess(0);
break;
}
}
return false;
}
// Returns true if we want to just exit the application.
// Returns false if the monarch dies, and we need to elect a new one.
bool peasantAppLoop(AppState& state)
{
wil::unique_handle hMonarch{ OpenProcess(PROCESS_ALL_ACCESS, FALSE, static_cast<DWORD>(state.monarch.GetPID())) };
if (hMonarch.get() == nullptr)
{
const auto gle = GetLastError();
printf("\x1b[31mFailed to open the monarch process, error was %d\x1b[m\n", gle);
return false;
}
HANDLE handlesToWaitOn[2]{ hMonarch.get(), state.hInput };
bool exitRequested = false;
printf("Press `q` to quit\n");
while (!exitRequested)
{
auto waitResult = WaitForMultipleObjects(2, handlesToWaitOn, false, INFINITE);
switch (waitResult)
{
case WAIT_OBJECT_0 + 0: // handlesToWaitOn[0] was signaled
printf("THE KING IS \x1b[31mDEAD\x1b[m\n");
// Return false here - this will trigger us to find the new monarch
return false;
case WAIT_OBJECT_0 + 1: // handlesToWaitOn[1] was signaled
exitRequested = peasantReadInput(state);
break;
case WAIT_TIMEOUT:
printf("Wait timed out. This should be impossible.\n");
break;
// Return value is invalid.
default:
{
auto gle = GetLastError();
printf("WaitForMultipleObjects returned: %d\n", waitResult);
printf("Wait error: %d\n", gle);
ExitProcess(0);
}
}
}
printf("Bottom of peasantAppLoop\n");
return true;
}

View File

@ -0,0 +1,16 @@
<?xml version="1.0" encoding="utf-8"?>
<Project ToolsVersion="4.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<ImportGroup Label="PropertySheets" />
<PropertyGroup Label="UserMacros" />
<!--
To customize common C++/WinRT project properties:
* right-click the project node
* expand the Common Properties item
* select the C++/WinRT property page
For more advanced scenarios, and complete documentation, please see:
https://github.com/Microsoft/cppwinrt/tree/master/nuget
-->
<PropertyGroup />
<ItemDefinitionGroup />
</Project>

View File

@ -0,0 +1,114 @@
# Monarch/Peasant Sample
This directory contains a sample monarch/peasant application. This is a type of
application where a single "Monarch" can coordinate the actions of multiple
other "Peasant" processes, as described by the specs in [#7240] and [#8135].
This project is intended to be a standalone sample of how the architecture would
work, without involving the entirety of the Windows Terminal build. Eventually,
this architecture will be incorporated into `wt.exe` itself, to enable scenarios
like:
* Run `wt` in the current window ([#4472])
* Single Instance Mode ([#2227])
For an example of this sample running, see the below GIF:
![Gif of the MonarchPeasantSample](monarch-peasant-sample-000.gif)
This sample operates largely by printing to the console, to help the reader
understand how it's working through its logic.
## Usage
```
MonarchPeasantSample.exe [--session,-s session-id] [args...]
```
This will run a new instance of the MonarchPeasantSample.
One of the running `MonarchPeasantSample` processes will be the "Monarch",
responsible for coordinating the creation of windows between processes. It can
be toggled between different `glomToLastWindow` modes by pressing `m`. It can be
exited with `q`.
The other instances will be "peasants", who can be told to execute commandlines.
`q` will exit the process. Any other keypress will "activate" the peasant
window, informing the monarch that this peasant is the newly-active one.
If the `session-id` is provided on the commandline, then the monarch will try to
pass the provided `args` to the specified peasant process, accounting for the
current value of `glomToLastWindow`. See the spec in [#8135] for details of how
this argument works. The actual `args...` params are unused.
## Project layout
The code is vaguely separated into the following files, with the following
purposes. As this code isn't production-ready code, the layering isn't
particularly well organized nor enforced.
* `Monarch.idl/.h/.cpp`: Code for the WinRT Monarch object, responsible for
coordinating the Peasants
* `Peasant.idl/.h/.cpp`: Code for the WinRT Peasant object, which represents any
individual application instance.
* `AppState.h/.cpp`: This file contains some common state that's used throughout
the application, in order to help encapsulate it all in one place.
* `MonarchMain.cpp`: This file contains the main loop for the monarch process.
It needs to be able to process console input, and additionally wait on the
peasants, to know when they've died.
* `PeasantMain.cpp`: This file contains the main loop for the peasant process.
It needs to be able to wait on both console input, and on the Monarch process,
to be able to determine who the next monarch should be.
## Remaining TODOs
This project represents an _incomplete_ example, but one that covers enough of
the edge cases for the reader to comprehend the architecture. Below is a list of
things that are missing from the sample:
* [ ] The Monarch should store a stack for the MRU peasant, not just the single
MRU one
* [ ] The Monarch needs to `WaitForMultipleObjects` on Peasants, to remove them
from the map when they die
* [ ] After an "election", the entire MRU window state is lost, because it was
only stored in the current monarch. This needs to be distributed to all the
other Peasants when it changes.
* [ ] Theoretically, `ProposeCommandline` should use the CWD of the process
calling it. That's left unimplemented for brevity.
* [ ] Technically, the Monarch is also a Peasant, and any keypress should also
activate the Monarch window as the MRU one.
* [ ] We're storing a strictly-increasing int for to determine what the next ID
for a peasant should be. This seems silly, and like we could probably just
iterate to find the first gap in the map (I'm sure there's a better way of
doing this that I can't recall, don't tell my CS540 professor)
* [ ] The following steps causes an unexpected crash:
- Create a monarch(#1) & peasant(#2)
- activate the peasant(#2)
- exit the peasant(#2)
- try running `MonarchPeasantSample.exe -s 0` (or `-s 2`)
- THIS WILL FAIL, but it _should_ just run the commandline in the monarch
(in the case of `-s 0`) or in a new window (in the `-s 1` case)
- The reason this fails is because the Monarch tries to call
`Peasant::ExecuteCommandline`, but the Peasant object doesn't actually
exist anymore (the process is dead!). Three fixes will help here:
- wrap all calls to peasants with try/catch's
- Do the "keep a MRU stack" thing (so we know `-s 0` is now the monarch)
- Do the "wait on Peasant processes" thing, to know to pop #2 from the
map and MRU stack.
## Utilities
Use the following helper script to open a new Terminal window with 4 splits - 3
running instances of MonarchPeasantSample.exe, and a fourth which can be used to
issue commands.
```cmd
pushd %OPENCON%\bin\x64\Debug\MonarchPeasantSample
wt -d . cmd /k MonarchPeasantSample.exe ; sp -d . cmd /k MonarchPeasantSample.exe ; sp -d . cmd /k MonarchPeasantSample.exe ; sp -d .
popd
```
[#2227]: https://github.com/microsoft/terminal/issues/2227
[#4472]: https://github.com/microsoft/terminal/issues/4472
[#7240]: https://github.com/microsoft/terminal/pull/7240
[#8135]: https://github.com/microsoft/terminal/pull/8135

View File

@ -0,0 +1,186 @@
#include "pch.h"
#include "SampleMonarch.h"
#include "Monarch.g.cpp"
#include "../../types/inc/utils.hpp"
using namespace winrt;
using namespace winrt::Windows::Foundation;
using namespace ::Microsoft::Console;
namespace winrt::MonarchPeasantSample::implementation
{
Monarch::Monarch()
{
printf("Instantiated a Monarch\n");
}
Monarch::~Monarch()
{
printf("~Monarch()\n");
}
uint64_t Monarch::GetPID()
{
return GetCurrentProcessId();
}
uint64_t Monarch::AddPeasant(winrt::MonarchPeasantSample::IPeasant peasant)
{
// This whole algorithm is terrible. There's gotta be a better way
// of finding the first opening in a non-consecutive map of int->object
auto providedID = peasant.GetID();
if (providedID == 0)
{
peasant.AssignID(_nextPeasantID++);
printf("Assigned the peasant the ID %lld\n", peasant.GetID());
}
else
{
printf("Peasant already had an ID, %lld\n", peasant.GetID());
_nextPeasantID = providedID >= _nextPeasantID ? providedID + 1 : _nextPeasantID;
}
auto newPeasantsId = peasant.GetID();
_peasants[newPeasantsId] = peasant;
_setMostRecentPeasant(newPeasantsId);
printf("(the next new peasant will get the ID %lld)\n", _nextPeasantID);
peasant.WindowActivated({ this, &Monarch::_peasantWindowActivated });
return newPeasantsId;
}
void Monarch::_peasantWindowActivated(const winrt::Windows::Foundation::IInspectable& sender,
const winrt::Windows::Foundation::IInspectable& /*args*/)
{
if (auto peasant{ sender.try_as<winrt::MonarchPeasantSample::Peasant>() })
{
auto theirID = peasant.GetID();
_setMostRecentPeasant(theirID);
}
}
winrt::MonarchPeasantSample::IPeasant Monarch::_getPeasant(uint64_t peasantID)
{
auto peasantSearch = _peasants.find(peasantID);
return peasantSearch == _peasants.end() ? nullptr : peasantSearch->second;
}
void Monarch::_setMostRecentPeasant(const uint64_t peasantID)
{
_mostRecentPeasant = peasantID;
printf("\x1b[90mThe most recent peasant is now \x1b[m#%llu\n", _mostRecentPeasant);
}
void Monarch::SetSelfID(const uint64_t selfID)
{
this->_thisPeasantID = selfID;
// Right now, the monarch assumes the role of the most recent
// window. If the monarch dies, and a new monarch takes over, then the
// entire stack of MRU windows will go with it. That's not what you
// want!
//
// In the real app, we'll have each window also track the timestamp it
// was activated at, and the monarch will cache these. So a new monarch
// could re-query these last activated timestamps, and reconstruct the
// MRU stack.
//
// This is a sample though, and we're not too worried about complete
// correctness here.
_setMostRecentPeasant(_thisPeasantID);
}
bool Monarch::ProposeCommandline(array_view<const winrt::hstring> args, winrt::hstring cwd)
{
auto argsProcessed = 0;
std::wstring fullCmdline;
for (const auto& arg : args)
{
fullCmdline += argsProcessed++ == 0 ? L"sample.exe" : arg;
fullCmdline += L" ";
}
wprintf(L"\x1b[36mProposed Commandline\x1b[m: \"");
wprintf(fullCmdline.c_str());
wprintf(L"\"\n");
bool createNewWindow = true;
if (args.size() >= 3)
{
// We'll need three args at least - [MonarchPeasantSample.exe, -s,
// id] to be able to have a session ID passed on the commandline.
if (args[1] == L"-s" || args[1] == L"--session")
{
auto sessionId = std::stoi({ args[2].data(), args[2].size() });
printf("Found a commandline intended for session %d\n", sessionId);
if (sessionId < 0)
{
printf("That certainly isn't a valid ID, they should make a new window.\n");
createNewWindow = true;
}
else if (sessionId == 0)
{
printf("Session 0 is actually #%llu\n", _mostRecentPeasant);
if (auto mruPeasant = _getPeasant(_mostRecentPeasant))
{
mruPeasant.ExecuteCommandline(args, cwd);
createNewWindow = false;
}
}
else
{
if (auto otherPeasant = _getPeasant(sessionId))
{
otherPeasant.ExecuteCommandline(args, cwd);
createNewWindow = false;
}
else
{
printf("I couldn't find a peasant for that ID, they should make a new window.\n");
}
}
}
}
else if (_windowingBehavior == WindowingBehavior::UseExisting)
{
if (auto mruPeasant = _getPeasant(_mostRecentPeasant))
{
mruPeasant.ExecuteCommandline(args, cwd);
createNewWindow = false;
}
}
else
{
printf("They definitely weren't an existing process. They should make a new window.\n");
}
return createNewWindow;
}
void Monarch::ToggleWindowingBehavior()
{
switch (_windowingBehavior)
{
case WindowingBehavior::UseNew:
_windowingBehavior = WindowingBehavior::UseExisting;
break;
case WindowingBehavior::UseExisting:
_windowingBehavior = WindowingBehavior::UseNew;
break;
}
printf("windowingBehavior: ");
switch (_windowingBehavior)
{
case WindowingBehavior::UseNew:
printf("useNew");
break;
case WindowingBehavior::UseExisting:
printf("useExisting");
break;
}
printf("\n");
}
}

View File

@ -0,0 +1,57 @@
#pragma once
#include "Monarch.g.h"
#include "SamplePeasant.h"
#include "../cascadia/inc/cppwinrt_utils.h"
// {50dba6cd-2222-4b12-8363-5e06f5d0082c}
constexpr GUID Monarch_clsid{
0x50dba6cd,
0x2222,
0x4b12,
{ 0x83, 0x63, 0x5e, 0x06, 0xf5, 0xd0, 0x08, 0x2c }
};
enum class WindowingBehavior : uint64_t
{
UseNew = 0,
UseExisting = 1,
};
namespace winrt::MonarchPeasantSample::implementation
{
struct Monarch : public MonarchT<Monarch>
{
Monarch();
~Monarch();
uint64_t GetPID();
uint64_t AddPeasant(winrt::MonarchPeasantSample::IPeasant peasant);
void SetSelfID(const uint64_t selfID);
bool ProposeCommandline(array_view<const winrt::hstring> args, winrt::hstring cwd);
void ToggleWindowingBehavior();
private:
uint64_t _nextPeasantID{ 1 };
uint64_t _thisPeasantID{ 0 };
uint64_t _mostRecentPeasant{ 0 };
WindowingBehavior _windowingBehavior{ WindowingBehavior::UseNew };
std::unordered_map<uint64_t, winrt::MonarchPeasantSample::IPeasant> _peasants;
winrt::MonarchPeasantSample::IPeasant _getPeasant(uint64_t peasantID);
void _setMostRecentPeasant(const uint64_t peasantID);
void _peasantWindowActivated(const winrt::Windows::Foundation::IInspectable& sender,
const winrt::Windows::Foundation::IInspectable& args);
};
}
namespace winrt::MonarchPeasantSample::factory_implementation
{
struct Monarch : MonarchT<Monarch, implementation::Monarch>
{
};
}

View File

@ -0,0 +1,12 @@
import "SamplePeasant.idl";
namespace MonarchPeasantSample
{
[default_interface] runtimeclass Monarch {
Monarch();
UInt64 GetPID();
UInt64 AddPeasant(IPeasant peasant);
Boolean ProposeCommandline(String[] args, String cwd);
};
}

View File

@ -0,0 +1,51 @@
#include "pch.h"
#include "SamplePeasant.h"
#include "Peasant.g.cpp"
#include "../../types/inc/utils.hpp"
using namespace winrt;
using namespace winrt::Windows::Foundation;
using namespace ::Microsoft::Console;
namespace winrt::MonarchPeasantSample::implementation
{
Peasant::Peasant()
{
}
void Peasant::AssignID(uint64_t id)
{
_id = id;
}
uint64_t Peasant::GetID()
{
return _id;
}
uint64_t Peasant::GetPID()
{
return GetCurrentProcessId();
}
bool Peasant::ExecuteCommandline(winrt::array_view<const winrt::hstring> args, winrt::hstring currentDirectory)
{
auto argsProcessed = 0;
std::wstring fullCmdline;
for (const auto& arg : args)
{
fullCmdline += argsProcessed++ == 0 ? L"sample.exe" : arg;
fullCmdline += L" ";
}
wprintf(L"\x1b[32mExecuted Commandline\x1b[m: \"");
wprintf(fullCmdline.c_str());
wprintf(L"\"\n");
return true;
}
void Peasant::raiseActivatedEvent()
{
_WindowActivatedHandlers(*this, nullptr);
}
}

View File

@ -0,0 +1,32 @@
#pragma once
#include "Peasant.g.h"
#include "../cascadia/inc/cppwinrt_utils.h"
namespace winrt::MonarchPeasantSample::implementation
{
struct Peasant : public PeasantT<Peasant>
{
Peasant();
void AssignID(uint64_t id);
uint64_t GetID();
uint64_t GetPID();
bool ExecuteCommandline(winrt::array_view<const winrt::hstring> args, winrt::hstring currentDirectory);
void raiseActivatedEvent();
TYPED_EVENT(WindowActivated, winrt::Windows::Foundation::IInspectable, winrt::Windows::Foundation::IInspectable);
private:
uint64_t _id{ 0 };
};
}
namespace winrt::MonarchPeasantSample::factory_implementation
{
struct Peasant : PeasantT<Peasant, implementation::Peasant>
{
};
}

View File

@ -0,0 +1,18 @@
namespace MonarchPeasantSample
{
interface IPeasant
{
void AssignID(UInt64 id);
UInt64 GetID();
UInt64 GetPID();
Boolean ExecuteCommandline(String[] args, String currentDirectory);
event Windows.Foundation.TypedEventHandler<Object, Object> WindowActivated;
};
[default_interface] runtimeclass Peasant : IPeasant
{
Peasant();
};
}

View File

@ -0,0 +1,168 @@
#include "pch.h"
#include "AppState.h"
#include "../../types/inc/utils.hpp"
using namespace winrt;
using namespace winrt::Windows::Foundation;
using namespace ::Microsoft::Console;
////////////////////////////////////////////////////////////////////////////////
// This seems like a hack, but it works.
//
// This class factory works so that there's only ever one instance of a Monarch
// per-process. Once the first monarch is created, we'll stash it in g_weak.
// Future callers who try to instantiate a Monarch will get the one that's
// already been made.
//
// I'm sure there's a better way to do this with WRL, but I'm not familiar
// enough with WRL to know for sure.
winrt::weak_ref<MonarchPeasantSample::implementation::Monarch> g_weak{ nullptr };
struct MonarchFactory : implements<MonarchFactory, IClassFactory>
{
MonarchFactory() = default;
HRESULT __stdcall CreateInstance(IUnknown* outer, GUID const& iid, void** result) noexcept final
{
*result = nullptr;
if (outer)
{
return CLASS_E_NOAGGREGATION;
}
if (!g_weak)
{
// Create a new Monarch instance
auto strong = make_self<MonarchPeasantSample::implementation::Monarch>();
g_weak = (*strong).get_weak();
return strong.as(iid, result);
}
else
{
// We already instantiated one Monarch, let's just return that one!
auto strong = g_weak.get();
return strong.as(iid, result);
}
}
HRESULT __stdcall LockServer(BOOL) noexcept final
{
return S_OK;
}
};
////////////////////////////////////////////////////////////////////////////////
// Function Description:
// - Register the Monarch object with COM. This allows other processes to create
// Monarch's in our process space with CoCreateInstance and the Monarch_clsid.
DWORD registerAsMonarch()
{
DWORD registrationHostClass{};
check_hresult(CoRegisterClassObject(Monarch_clsid,
make<MonarchFactory>().get(),
CLSCTX_LOCAL_SERVER,
REGCLS_MULTIPLEUSE,
&registrationHostClass));
return registrationHostClass;
}
// Function Description:
// - Called when the old monarch dies. Create a new connection to the new
// monarch. This might be us! If we're the new monarch, then update the
// Monarch to know which Peasant it came from. Otherwise, tell the new monarch
// that we exist.
void electNewMonarch(AppState& state)
{
state.monarch = AppState::instantiateMonarch();
bool isMonarch = state.areWeTheKing(true);
printf("LONG LIVE THE %sKING\x1b[m\n", isMonarch ? "\x1b[33m" : "");
if (isMonarch)
{
state.remindKingWhoTheyAre(state.peasant);
}
else
{
// Add us to the new monarch
state.monarch.AddPeasant(state.peasant);
}
}
void appLoop(AppState& state)
{
auto dwRegistration = registerAsMonarch();
// IMPORTANT! Tear down the registration as soon as we exit. If we're not a
// real peasant window (the monarch passed our commandline to someone else),
// then the monarch dies, we don't want our registration becoming the active
// monarch!
auto cleanup = wil::scope_exit([&]() {
check_hresult(CoRevokeClassObject(dwRegistration));
});
// Tricky - first, we have to ask the monarch to handle the commandline.
// They will tell us if we need to create a peasant.
state.createMonarch();
// processCommandline will return true if we should exit early.
if (state.processCommandline())
{
return;
}
bool isMonarch = state.areWeTheKing(true);
bool exitRequested = false;
// (monarch|peasant)AppLoop will return when they've run to completion. If
// they return true, then just exit the application (the user might have
// pressed 'q' to exit). If the peasant returns false, then it detected the
// monarch died. Attempt to elect a new one.
while (!exitRequested)
{
if (isMonarch)
{
exitRequested = monarchAppLoop(state);
}
else
{
exitRequested = peasantAppLoop(state);
if (!exitRequested)
{
electNewMonarch(state);
isMonarch = state.areWeTheKing(false);
}
}
}
}
int main(int argc, char** argv)
{
AppState state;
state.initializeState();
// Collect up all the commandline arguments
printf("args:[");
for (auto& elem : wil::make_range(argv, argc))
{
printf("%s, ", elem);
// This is obviously a bad way of converting args to a vector of
// hstrings, but it'll do for now.
state.args.emplace_back(winrt::to_hstring(elem));
}
printf("]\n");
try
{
appLoop(state);
}
catch (hresult_error const& e)
{
printf("Error: %ls\n", e.message().c_str());
}
printf("We've left the app. Press any key to close.\n");
const auto ch = _getch();
ch;
printf("Exiting client\n");
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.6 MiB

View File

@ -0,0 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<packages>
<package id="Microsoft.Windows.CppWinRT" version="2.0.200609.3" targetFramework="native" />
</packages>

View File

@ -0,0 +1 @@
#include "pch.h"

View File

@ -0,0 +1,26 @@
#pragma once
#define WIN32_LEAN_AND_MEAN
// Manually include til after we include Windows.Foundation to give it winrt superpowers
#define BLOCK_TIL
#include <wil/cppwinrt.h>
#undef max
#undef min
#include "LibraryIncludes.h"
// This is inexplicable, but for whatever reason, cppwinrt conflicts with the
// SDK definition of this function, so the only fix is to undef it.
// from WinBase.h
// Windows::UI::Xaml::Media::Animation::IStoryboard::GetCurrentTime
#ifdef GetCurrentTime
#undef GetCurrentTime
#endif
#include <Unknwn.h>
#include <winrt/Windows.Foundation.h>
#include <winrt/Windows.Foundation.Collections.h>
// Manually include til after we include Windows.Foundation to give it winrt superpowers
#include "til.h"
#include <conio.h>