From 906edf7002b8ccf4d819fcc6f45a68f7815b3252 Mon Sep 17 00:00:00 2001 From: Michael Niksa Date: Fri, 26 Mar 2021 15:09:49 -0700 Subject: [PATCH] Implement Default Terminal (#7489) - Implements the default application behavior and handoff mechanisms between console and terminal. The inbox portion is done already. This adds the ability for our OpenConsole.exe to accept the incoming server connection from the Windows OS, stand up a PTY session, start the Windows Terminal as a listener for an incoming connection, and then send it the incoming PTY connection for it to launch a tab. - The tab is launched with default settings at the moment. - You must configure the default application using the `conhost.exe` propsheet or with the registry keys. Finishing the setting inside Windows Terminal will be a todo after this is complete. The OS Settings panel work to surface this setting is a dependency delivered by another team and you will not see it here. ## Validation Steps Performed - [x] Manual adjust of registry keys to the delegation conhost/terminal behavior - [x] Adjustment of the delegation options with the propsheet - [x] Launching things from the run box manually and watching them show in Terminal - [x] Launching things from shortcuts and watching them show in the Terminal Documentation on how it works will be a TODO post completion in #9462 References #7414 - Default Terminal spec Closes #492 --- .github/actions/spelling/dictionary/apis.txt | 6 + .github/actions/spelling/expect/expect.txt | 2 + .vscode/tasks.json | 17 +- OpenConsole.sln | 70 ++-- .../CascadiaPackage/CascadiaPackage.wapproj | 1 + .../CascadiaPackage/Package-Dev.appxmanifest | 36 +- .../CascadiaPackage/Package-Pre.appxmanifest | 35 ++ .../CascadiaPackage/Package.appxmanifest | 36 +- .../TerminalApp/AppCommandlineArgs.cpp | 48 ++- src/cascadia/TerminalApp/AppCommandlineArgs.h | 2 + src/cascadia/TerminalApp/AppLogic.cpp | 11 + .../Resources/en-US/Resources.resw | 54 +-- src/cascadia/TerminalApp/TerminalPage.cpp | 73 +++- src/cascadia/TerminalApp/TerminalPage.h | 9 +- .../TerminalConnection/CTerminalHandoff.cpp | 108 ++++++ .../TerminalConnection/CTerminalHandoff.h | 56 +++ .../TerminalConnection/ConptyConnection.cpp | 61 +++- .../TerminalConnection/ConptyConnection.h | 13 + .../TerminalConnection/ConptyConnection.idl | 10 +- .../ITerminalConnection.idl | 2 + .../TerminalConnection.vcxproj | 7 +- .../TerminalConnection.vcxproj.filters | 6 +- .../TerminalSettingsEditor/Launch.cpp | 17 + src/cascadia/TerminalSettingsEditor/Launch.h | 4 + .../TerminalSettingsEditor/Launch.idl | 4 +- .../TerminalSettingsEditor/Launch.xaml | 10 + .../Resources/en-US/Resources.resw | 8 + src/host/ConsoleArguments.cpp | 19 + src/host/ConsoleArguments.hpp | 14 +- src/host/exe/CConsoleHandoff.cpp | 62 ++++ src/host/exe/CConsoleHandoff.h | 42 +++ src/host/exe/Host.EXE.vcxproj | 8 +- src/host/exe/Host.EXE.vcxproj.filters | 8 +- src/host/exe/exemain.cpp | 334 ++++++++++++++++++ src/host/exe/sources | 3 +- src/host/exemain.cpp | 227 ------------ src/host/globals.h | 1 + src/host/host-common.vcxitems | 2 +- src/host/lib/hostlib.vcxproj | 2 +- src/host/lib/hostlib.vcxproj.filters | 32 +- src/host/proxy/Host.Proxy.vcxproj | 43 ++- src/host/proxy/Host.Proxy.vcxproj.filters | 25 +- src/host/proxy/IConsoleHandoff.idl | 4 + src/host/proxy/ITerminalHandoff.idl | 16 + src/host/proxy/OpenConsoleProxy.def | 8 + src/host/srvinit.cpp | 176 ++++++++- src/host/srvinit.h | 4 + src/host/ut_host/ConsoleArgumentsTests.cpp | 150 +++++--- src/internal/stubs.cpp | 6 +- src/server/IoDispatchers.cpp | 14 + 50 files changed, 1483 insertions(+), 423 deletions(-) create mode 100644 src/cascadia/TerminalConnection/CTerminalHandoff.cpp create mode 100644 src/cascadia/TerminalConnection/CTerminalHandoff.h create mode 100644 src/host/exe/CConsoleHandoff.cpp create mode 100644 src/host/exe/CConsoleHandoff.h create mode 100644 src/host/exe/exemain.cpp delete mode 100644 src/host/exemain.cpp create mode 100644 src/host/proxy/ITerminalHandoff.idl create mode 100644 src/host/proxy/OpenConsoleProxy.def diff --git a/.github/actions/spelling/dictionary/apis.txt b/.github/actions/spelling/dictionary/apis.txt index e6c7a1957..378c6786b 100644 --- a/.github/actions/spelling/dictionary/apis.txt +++ b/.github/actions/spelling/dictionary/apis.txt @@ -11,6 +11,7 @@ CYICON D2DERR_SHADER_COMPILE_FAILED dataobject DERR +dlldata environstrings EXPCMDFLAGS EXPCMDSTATE @@ -83,6 +84,7 @@ schandle semver serializer shobjidl +SINGLEUSE SHOWMINIMIZED SIZENS smoothstep @@ -93,6 +95,8 @@ spsc sregex STDCPP strchr +STDMETHOD +Stubless Subheader Subpage UPDATEINIFILE @@ -108,8 +112,10 @@ UPDATEINIFILE userenv wcsstr wcstoui +winmain wpc wsregex +wwinmain XDocument XElement xlocmes diff --git a/.github/actions/spelling/expect/expect.txt b/.github/actions/spelling/expect/expect.txt index c3aba85a2..148e47080 100644 --- a/.github/actions/spelling/expect/expect.txt +++ b/.github/actions/spelling/expect/expect.txt @@ -433,6 +433,7 @@ cstring cstyle csv CSwitch +CTerminal CText ctime ctl @@ -571,6 +572,7 @@ defaultsettings DEFAULTTONEAREST DEFAULTTONULL DEFAULTTOPRIMARY +DEFCON defectdefs DEFERERASE deff diff --git a/.vscode/tasks.json b/.vscode/tasks.json index 827e4c407..e9942e0bb 100644 --- a/.vscode/tasks.json +++ b/.vscode/tasks.json @@ -16,7 +16,9 @@ "${workspaceFolder}\\OpenConsole.sln", "/p:Configuration=${input:configChoice}", "/p:Platform=${input:platformChoice}", + "/p:AppxSymbolPackageEnabled=false", // This takes a long time, so false if we don't really need it. "/t:$target", + "/m", // Parallel builds "/verbosity:minimal" ], "problemMatcher": ["$msCompile"], @@ -46,8 +48,7 @@ ], "problemMatcher": ["$msCompile"], "group": { - "kind": "build", - "isDefault": true + "kind": "build" } }, { @@ -57,6 +58,18 @@ "args": [ ], "problemMatcher": ["$msCompile"], + }, + { + "type": "process", + "label": "Run Code Format", + "command": "powershell.exe", + "args": [ + "-Command", + "Import-Module ${workspaceFolder}\\tools\\OpenConsole.psm1;", + "Set-MsBuildDevEnvironment;", + "Invoke-CodeFormat", + ], + "problemMatcher": ["$msCompile"], } ], "inputs":[ diff --git a/OpenConsole.sln b/OpenConsole.sln index 71bc8ce92..0cdb34c7e 100644 --- a/OpenConsole.sln +++ b/OpenConsole.sln @@ -12,7 +12,9 @@ Project("{C7167F0D-BC9F-4E6E-AFE1-012C56B48DB5}") = "CascadiaPackage", "src\casc EndProject Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "Host.EXE", "src\host\exe\Host.EXE.vcxproj", "{9CBD7DFA-1754-4A9D-93D7-857A9D17CB1B}" ProjectSection(ProjectDependencies) = postProject + {71CC9D78-BA29-4D93-946F-BEF5D9A3A6EF} = {71CC9D78-BA29-4D93-946F-BEF5D9A3A6EF} {0CF235BD-2DA0-407E-90EE-C467E8BBC714} = {0CF235BD-2DA0-407E-90EE-C467E8BBC714} + {5D23E8E1-3C64-4CC1-A8F7-6861677F7239} = {5D23E8E1-3C64-4CC1-A8F7-6861677F7239} EndProjectSection EndProject Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "PropertiesLibrary", "src\propslib\propslib.vcxproj", "{345FD5A4-B32B-4F29-BD1C-B033BD2C35CC}" @@ -64,6 +66,7 @@ EndProject Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "Host", "src\host\lib\hostlib.vcxproj", "{06EC74CB-9A12-429C-B551-8562EC954746}" ProjectSection(ProjectDependencies) = postProject {18D09A24-8240-42D6-8CB6-236EEE820263} = {18D09A24-8240-42D6-8CB6-236EEE820263} + {71CC9D78-BA29-4D93-946F-BEF5D9A3A6EF} = {71CC9D78-BA29-4D93-946F-BEF5D9A3A6EF} {0CF235BD-2DA0-407E-90EE-C467E8BBC714} = {0CF235BD-2DA0-407E-90EE-C467E8BBC714} EndProjectSection EndProject @@ -104,6 +107,9 @@ EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "_Build Common", "_Build Common", "{04170EEF-983A-4195-BFEF-2321E5E38A1E}" EndProject Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "Server", "src\server\lib\server.vcxproj", "{18D09A24-8240-42D6-8CB6-236EEE820262}" + ProjectSection(ProjectDependencies) = postProject + {71CC9D78-BA29-4D93-946F-BEF5D9A3A6EF} = {71CC9D78-BA29-4D93-946F-BEF5D9A3A6EF} + EndProjectSection EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Host.Tests.UIA", "src\host\ft_uia\Host.Tests.UIA.csproj", "{C17E1BF3-9D34-4779-9458-A8EF98CC5662}" ProjectSection(ProjectDependencies) = postProject @@ -369,7 +375,7 @@ Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "UnitTests_Remoting", "src\c EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "wpf", "wpf", "{4DAF0299-495E-4CD1-A982-9BAC16A45932}" EndProject -Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "OpenConsoleProxy", "src\host\proxy\Host.Proxy.vcxproj", "{E437B604-3E98-4F40-A927-E173E818EA4B}" +Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "OpenConsoleProxy", "src\host\proxy\Host.Proxy.vcxproj", "{71CC9D78-BA29-4D93-946F-BEF5D9A3A6EF}" EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Window", "Window", "{2D17E75D-2DDC-42C4-AD70-704D95A937AE}" EndProject @@ -2580,36 +2586,36 @@ Global {68A10CD3-AA64-465B-AF5F-ED4E9700543C}.Release|x64.Build.0 = Release|x64 {68A10CD3-AA64-465B-AF5F-ED4E9700543C}.Release|x86.ActiveCfg = Release|Win32 {68A10CD3-AA64-465B-AF5F-ED4E9700543C}.Release|x86.Build.0 = Release|Win32 - {E437B604-3E98-4F40-A927-E173E818EA4B}.AuditMode|Any CPU.ActiveCfg = AuditMode|Win32 - {E437B604-3E98-4F40-A927-E173E818EA4B}.AuditMode|ARM.ActiveCfg = AuditMode|Win32 - {E437B604-3E98-4F40-A927-E173E818EA4B}.AuditMode|ARM64.ActiveCfg = AuditMode|ARM64 - {E437B604-3E98-4F40-A927-E173E818EA4B}.AuditMode|ARM64.Build.0 = AuditMode|ARM64 - {E437B604-3E98-4F40-A927-E173E818EA4B}.AuditMode|DotNet_x64Test.ActiveCfg = AuditMode|Win32 - {E437B604-3E98-4F40-A927-E173E818EA4B}.AuditMode|DotNet_x86Test.ActiveCfg = AuditMode|Win32 - {E437B604-3E98-4F40-A927-E173E818EA4B}.AuditMode|x64.ActiveCfg = AuditMode|x64 - {E437B604-3E98-4F40-A927-E173E818EA4B}.AuditMode|x64.Build.0 = AuditMode|x64 - {E437B604-3E98-4F40-A927-E173E818EA4B}.AuditMode|x86.ActiveCfg = AuditMode|Win32 - {E437B604-3E98-4F40-A927-E173E818EA4B}.AuditMode|x86.Build.0 = AuditMode|Win32 - {E437B604-3E98-4F40-A927-E173E818EA4B}.Debug|Any CPU.ActiveCfg = Debug|Win32 - {E437B604-3E98-4F40-A927-E173E818EA4B}.Debug|ARM.ActiveCfg = Debug|Win32 - {E437B604-3E98-4F40-A927-E173E818EA4B}.Debug|ARM64.ActiveCfg = Debug|ARM64 - {E437B604-3E98-4F40-A927-E173E818EA4B}.Debug|ARM64.Build.0 = Debug|ARM64 - {E437B604-3E98-4F40-A927-E173E818EA4B}.Debug|DotNet_x64Test.ActiveCfg = Debug|Win32 - {E437B604-3E98-4F40-A927-E173E818EA4B}.Debug|DotNet_x86Test.ActiveCfg = Debug|Win32 - {E437B604-3E98-4F40-A927-E173E818EA4B}.Debug|x64.ActiveCfg = Debug|x64 - {E437B604-3E98-4F40-A927-E173E818EA4B}.Debug|x64.Build.0 = Debug|x64 - {E437B604-3E98-4F40-A927-E173E818EA4B}.Debug|x86.ActiveCfg = Debug|Win32 - {E437B604-3E98-4F40-A927-E173E818EA4B}.Debug|x86.Build.0 = Debug|Win32 - {E437B604-3E98-4F40-A927-E173E818EA4B}.Release|Any CPU.ActiveCfg = Release|Win32 - {E437B604-3E98-4F40-A927-E173E818EA4B}.Release|ARM.ActiveCfg = Release|Win32 - {E437B604-3E98-4F40-A927-E173E818EA4B}.Release|ARM64.ActiveCfg = Release|ARM64 - {E437B604-3E98-4F40-A927-E173E818EA4B}.Release|ARM64.Build.0 = Release|ARM64 - {E437B604-3E98-4F40-A927-E173E818EA4B}.Release|DotNet_x64Test.ActiveCfg = Release|Win32 - {E437B604-3E98-4F40-A927-E173E818EA4B}.Release|DotNet_x86Test.ActiveCfg = Release|Win32 - {E437B604-3E98-4F40-A927-E173E818EA4B}.Release|x64.ActiveCfg = Release|x64 - {E437B604-3E98-4F40-A927-E173E818EA4B}.Release|x64.Build.0 = Release|x64 - {E437B604-3E98-4F40-A927-E173E818EA4B}.Release|x86.ActiveCfg = Release|Win32 - {E437B604-3E98-4F40-A927-E173E818EA4B}.Release|x86.Build.0 = Release|Win32 + {71CC9D78-BA29-4D93-946F-BEF5D9A3A6EF}.AuditMode|Any CPU.ActiveCfg = AuditMode|Win32 + {71CC9D78-BA29-4D93-946F-BEF5D9A3A6EF}.AuditMode|ARM.ActiveCfg = AuditMode|Win32 + {71CC9D78-BA29-4D93-946F-BEF5D9A3A6EF}.AuditMode|ARM64.ActiveCfg = AuditMode|ARM64 + {71CC9D78-BA29-4D93-946F-BEF5D9A3A6EF}.AuditMode|ARM64.Build.0 = AuditMode|ARM64 + {71CC9D78-BA29-4D93-946F-BEF5D9A3A6EF}.AuditMode|DotNet_x64Test.ActiveCfg = AuditMode|Win32 + {71CC9D78-BA29-4D93-946F-BEF5D9A3A6EF}.AuditMode|DotNet_x86Test.ActiveCfg = AuditMode|Win32 + {71CC9D78-BA29-4D93-946F-BEF5D9A3A6EF}.AuditMode|x64.ActiveCfg = AuditMode|x64 + {71CC9D78-BA29-4D93-946F-BEF5D9A3A6EF}.AuditMode|x64.Build.0 = AuditMode|x64 + {71CC9D78-BA29-4D93-946F-BEF5D9A3A6EF}.AuditMode|x86.ActiveCfg = AuditMode|Win32 + {71CC9D78-BA29-4D93-946F-BEF5D9A3A6EF}.AuditMode|x86.Build.0 = AuditMode|Win32 + {71CC9D78-BA29-4D93-946F-BEF5D9A3A6EF}.Debug|Any CPU.ActiveCfg = Debug|Win32 + {71CC9D78-BA29-4D93-946F-BEF5D9A3A6EF}.Debug|ARM.ActiveCfg = Debug|Win32 + {71CC9D78-BA29-4D93-946F-BEF5D9A3A6EF}.Debug|ARM64.ActiveCfg = Debug|ARM64 + {71CC9D78-BA29-4D93-946F-BEF5D9A3A6EF}.Debug|ARM64.Build.0 = Debug|ARM64 + {71CC9D78-BA29-4D93-946F-BEF5D9A3A6EF}.Debug|DotNet_x64Test.ActiveCfg = Debug|Win32 + {71CC9D78-BA29-4D93-946F-BEF5D9A3A6EF}.Debug|DotNet_x86Test.ActiveCfg = Debug|Win32 + {71CC9D78-BA29-4D93-946F-BEF5D9A3A6EF}.Debug|x64.ActiveCfg = Debug|x64 + {71CC9D78-BA29-4D93-946F-BEF5D9A3A6EF}.Debug|x64.Build.0 = Debug|x64 + {71CC9D78-BA29-4D93-946F-BEF5D9A3A6EF}.Debug|x86.ActiveCfg = Debug|Win32 + {71CC9D78-BA29-4D93-946F-BEF5D9A3A6EF}.Debug|x86.Build.0 = Debug|Win32 + {71CC9D78-BA29-4D93-946F-BEF5D9A3A6EF}.Release|Any CPU.ActiveCfg = Release|Win32 + {71CC9D78-BA29-4D93-946F-BEF5D9A3A6EF}.Release|ARM.ActiveCfg = Release|Win32 + {71CC9D78-BA29-4D93-946F-BEF5D9A3A6EF}.Release|ARM64.ActiveCfg = Release|ARM64 + {71CC9D78-BA29-4D93-946F-BEF5D9A3A6EF}.Release|ARM64.Build.0 = Release|ARM64 + {71CC9D78-BA29-4D93-946F-BEF5D9A3A6EF}.Release|DotNet_x64Test.ActiveCfg = Release|Win32 + {71CC9D78-BA29-4D93-946F-BEF5D9A3A6EF}.Release|DotNet_x86Test.ActiveCfg = Release|Win32 + {71CC9D78-BA29-4D93-946F-BEF5D9A3A6EF}.Release|x64.ActiveCfg = Release|x64 + {71CC9D78-BA29-4D93-946F-BEF5D9A3A6EF}.Release|x64.Build.0 = Release|x64 + {71CC9D78-BA29-4D93-946F-BEF5D9A3A6EF}.Release|x86.ActiveCfg = Release|Win32 + {71CC9D78-BA29-4D93-946F-BEF5D9A3A6EF}.Release|x86.Build.0 = Release|Win32 EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -2701,7 +2707,7 @@ Global {27B5AAEB-A548-44CF-9777-F8BAA32AF7AE} = {2D17E75D-2DDC-42C4-AD70-704D95A937AE} {68A10CD3-AA64-465B-AF5F-ED4E9700543C} = {BDB237B6-1D1D-400F-84CC-40A58FA59C8E} {4DAF0299-495E-4CD1-A982-9BAC16A45932} = {59840756-302F-44DF-AA47-441A9D673202} - {E437B604-3E98-4F40-A927-E173E818EA4B} = {E8F24881-5E37-4362-B191-A3BA0ED7F4EB} + {71CC9D78-BA29-4D93-946F-BEF5D9A3A6EF} = {E8F24881-5E37-4362-B191-A3BA0ED7F4EB} {2D17E75D-2DDC-42C4-AD70-704D95A937AE} = {59840756-302F-44DF-AA47-441A9D673202} {77875138-BB08-49F9-8BB1-409C2150E0E1} = {59840756-302F-44DF-AA47-441A9D673202} {9921CA0A-320C-4460-8623-3A3196E7F4CB} = {59840756-302F-44DF-AA47-441A9D673202} diff --git a/src/cascadia/CascadiaPackage/CascadiaPackage.wapproj b/src/cascadia/CascadiaPackage/CascadiaPackage.wapproj index b10a88732..865774f32 100644 --- a/src/cascadia/CascadiaPackage/CascadiaPackage.wapproj +++ b/src/cascadia/CascadiaPackage/CascadiaPackage.wapproj @@ -70,6 +70,7 @@ + diff --git a/src/cascadia/CascadiaPackage/Package-Dev.appxmanifest b/src/cascadia/CascadiaPackage/Package-Dev.appxmanifest index a257a803a..ee36644e2 100644 --- a/src/cascadia/CascadiaPackage/Package-Dev.appxmanifest +++ b/src/cascadia/CascadiaPackage/Package-Dev.appxmanifest @@ -74,9 +74,43 @@ com.microsoft.windows.terminal.settings - + + + + {1F9F2BF5-5BC3-4F17-B0E6-912413F1F451} + + + + + + + {051F34EE-C1FD-4B19-AF75-9BA54648434C} + + + + + + + + + + + + + + + + diff --git a/src/cascadia/CascadiaPackage/Package-Pre.appxmanifest b/src/cascadia/CascadiaPackage/Package-Pre.appxmanifest index 11a093d95..aac5b64f3 100644 --- a/src/cascadia/CascadiaPackage/Package-Pre.appxmanifest +++ b/src/cascadia/CascadiaPackage/Package-Pre.appxmanifest @@ -75,8 +75,43 @@ Enabled="false" DisplayName="ms-resource:AppNamePre" /> + + + + {06EC847C-C0A5-46B8-92CB-7C92F6E35CD5} + + + + + + + {86633F1F-6454-40EC-89CE-DA4EBA977EE2} + + + + + + + + + + + + + + + diff --git a/src/cascadia/CascadiaPackage/Package.appxmanifest b/src/cascadia/CascadiaPackage/Package.appxmanifest index e6a4d7052..b7953a3e0 100644 --- a/src/cascadia/CascadiaPackage/Package.appxmanifest +++ b/src/cascadia/CascadiaPackage/Package.appxmanifest @@ -75,9 +75,43 @@ Enabled="false" DisplayName="ms-resource:AppName" /> - + + diff --git a/src/cascadia/TerminalApp/AppCommandlineArgs.cpp b/src/cascadia/TerminalApp/AppCommandlineArgs.cpp index 67b759068..6e68084e8 100644 --- a/src/cascadia/TerminalApp/AppCommandlineArgs.cpp +++ b/src/cascadia/TerminalApp/AppCommandlineArgs.cpp @@ -684,6 +684,18 @@ std::vector& AppCommandlineArgs::GetStartupActions() return _startupActions; } +// Method Description: +// - Returns whether we should start listening for inbound PTY connections +// coming from the operating system default application feature. +// Arguments: +// - +// Return Value: +// - True if the listener should be started. False otherwise. +bool AppCommandlineArgs::IsHandoffListener() const noexcept +{ + return _isHandoffListener; +} + // Method Description: // - Get the string of text that should be displayed to the user on exit. This // is usually helpful for cases where the user entered some sort of invalid @@ -726,17 +738,23 @@ bool AppCommandlineArgs::ShouldExitEarly() const noexcept // - void AppCommandlineArgs::ValidateStartupCommands() { - // If we parsed no commands, or the first command we've parsed is not a new - // tab action, prepend a new-tab command to the front of the list. - if (_startupActions.empty() || - _startupActions.front().Action() != ShortcutAction::NewTab) + // Only check over the actions list for the potential to add a new-tab + // command if we are not starting for the purposes of receiving an inbound + // handoff connection from the operating system. + if (!_isHandoffListener) { - // Build the NewTab action from the values we've parsed on the commandline. - NewTerminalArgs newTerminalArgs{}; - NewTabArgs args{ newTerminalArgs }; - ActionAndArgs newTabAction{ ShortcutAction::NewTab, args }; - // push the arg onto the front - _startupActions.insert(_startupActions.begin(), 1, newTabAction); + // If we parsed no commands, or the first command we've parsed is not a new + // tab action, prepend a new-tab command to the front of the list. + if (_startupActions.empty() || + _startupActions.front().Action() != ShortcutAction::NewTab) + { + // Build the NewTab action from the values we've parsed on the commandline. + NewTerminalArgs newTerminalArgs{}; + NewTabArgs args{ newTerminalArgs }; + ActionAndArgs newTabAction{ ShortcutAction::NewTab, args }; + // push the arg onto the front + _startupActions.insert(_startupActions.begin(), 1, newTabAction); + } } } @@ -760,6 +778,15 @@ std::optional AppComman // - 0 if the commandline was successfully parsed int AppCommandlineArgs::ParseArgs(winrt::array_view& args) { + for (const auto& arg : args) + { + if (arg == L"-Embedding") + { + _isHandoffListener = true; + return 0; + } + } + auto commands = ::TerminalApp::AppCommandlineArgs::BuildCommands(args); for (auto& cmdBlob : commands) @@ -864,6 +891,7 @@ void AppCommandlineArgs::FullResetState() _startupActions.clear(); _exitMessage = ""; _shouldExitEarly = false; + _isHandoffListener = false; _windowTarget = {}; } diff --git a/src/cascadia/TerminalApp/AppCommandlineArgs.h b/src/cascadia/TerminalApp/AppCommandlineArgs.h index 4c3456932..bc3d56f16 100644 --- a/src/cascadia/TerminalApp/AppCommandlineArgs.h +++ b/src/cascadia/TerminalApp/AppCommandlineArgs.h @@ -35,6 +35,7 @@ public: void ValidateStartupCommands(); std::vector& GetStartupActions(); + bool IsHandoffListener() const noexcept; const std::string& GetExitMessage(); bool ShouldExitEarly() const noexcept; @@ -104,6 +105,7 @@ private: const Commandline* _currentCommandline{ nullptr }; std::optional _launchMode{ std::nullopt }; + bool _isHandoffListener{ false }; std::vector _startupActions; std::string _exitMessage; bool _shouldExitEarly{ false }; diff --git a/src/cascadia/TerminalApp/AppLogic.cpp b/src/cascadia/TerminalApp/AppLogic.cpp index e7a10b3e4..fecccc750 100644 --- a/src/cascadia/TerminalApp/AppLogic.cpp +++ b/src/cascadia/TerminalApp/AppLogic.cpp @@ -1166,6 +1166,17 @@ namespace winrt::TerminalApp::implementation _hasCommandLineArguments = args.size() > 1; _appArgs.ValidateStartupCommands(); _root->SetStartupActions(_appArgs.GetStartupActions()); + + // Check if we were started as a COM server for inbound connections of console sessions + // coming out of the operating system default application feature. If so, + // tell TerminalPage to start the listener as we have to make sure it has the chance + // to register a handler to hear about the requests first and is all ready to receive + // them before the COM server registers itself. Otherwise, the request might come + // in and be routed to an event with no handlers or a non-ready Page. + if (_appArgs.IsHandoffListener()) + { + _root->SetInboundListener(); + } } return result; diff --git a/src/cascadia/TerminalApp/Resources/en-US/Resources.resw b/src/cascadia/TerminalApp/Resources/en-US/Resources.resw index 51b011c46..68dcb0da8 100644 --- a/src/cascadia/TerminalApp/Resources/en-US/Resources.resw +++ b/src/cascadia/TerminalApp/Resources/en-US/Resources.resw @@ -1,17 +1,17 @@  - diff --git a/src/cascadia/TerminalApp/TerminalPage.cpp b/src/cascadia/TerminalApp/TerminalPage.cpp index 0eefa036f..047572575 100644 --- a/src/cascadia/TerminalApp/TerminalPage.cpp +++ b/src/cascadia/TerminalApp/TerminalPage.cpp @@ -194,6 +194,9 @@ namespace winrt::TerminalApp::implementation // Hookup our event handlers to the ShortcutActionDispatch _RegisterActionCallbacks(); + // Hook up inbound connection event handler + TerminalConnection::ConptyConnection::NewConnection({ this, &TerminalPage::_OnNewConnection }); + //Event Bindings (Early) _newTabButton.Click([weakThis{ get_weak() }](auto&&, auto&&) { if (auto page{ weakThis.get() }) @@ -351,15 +354,29 @@ namespace winrt::TerminalApp::implementation if (_startupState == StartupState::NotInitialized) { _startupState = StartupState::InStartup; - if (_startupActions.Size() == 0) - { - _OpenNewTab(nullptr); + ProcessStartupActions(_startupActions, true); - _CompleteInitialization(); - } - else + // If we were told that the COM server needs to be started to listen for incoming + // default application connections, start it now. + // This MUST be done after we've registered the event listener for the new connections + // or the COM server might start receiving requests on another thread and dispatch + // them to nowhere. + if (_shouldStartInboundListener) { - ProcessStartupActions(_startupActions, true); + try + { + winrt::Microsoft::Terminal::TerminalConnection::ConptyConnection::StartInboundListener(); + } + // If we failed to start the listener, it will throw. + // We should fail fast here or the Terminal will be in a very strange state. + // We only start the listener if the Terminal was started with the COM server + // `-Embedding` flag and we make no tabs as a result. + // Therefore, if the listener cannot start itself up to make that tab with + // the inbound connection that caused the COM activation in the first place... + // we would be left with an empty terminal frame with no tabs. + // Instead, crash out so COM sees the server die and things unwind + // without a weird empty frame window. + CATCH_FAIL_FAST() } } } @@ -382,11 +399,6 @@ namespace winrt::TerminalApp::implementation const bool initial, const winrt::hstring cwd) { - // If there are no actions left, do nothing. - if (actions.Size() == 0) - { - return; - } auto weakThis{ get_weak() }; // Handle it on a subsequent pass of the UI thread. @@ -757,13 +769,16 @@ namespace winrt::TerminalApp::implementation // - newTerminalArgs: An object that may contain a blob of parameters to // control which profile is created and with possible other // configurations. See TerminalSettings::CreateWithNewTerminalArgs for more details. - void TerminalPage::_OpenNewTab(const NewTerminalArgs& newTerminalArgs) + // - existingConnection: An optional connection that is already established to a PTY + // for this tab to host instead of creating one. + // If not defined, the tab will create the connection. + void TerminalPage::_OpenNewTab(const NewTerminalArgs& newTerminalArgs, winrt::Microsoft::Terminal::TerminalConnection::ITerminalConnection existingConnection) try { const auto profileGuid{ _settings.GetProfileForArgs(newTerminalArgs) }; const auto settings{ TerminalSettings::CreateWithNewTerminalArgs(_settings, newTerminalArgs, *_bindings) }; - _CreateNewTabFromSettings(profileGuid, settings); + _CreateNewTabFromSettings(profileGuid, settings, existingConnection); const uint32_t tabCount = _tabs.Size(); const bool usedManualProfile = (newTerminalArgs != nullptr) && @@ -804,13 +819,14 @@ namespace winrt::TerminalApp::implementation // - Creates a new tab with the given settings. If the tab bar is not being // currently displayed, it will be shown. // Arguments: + // - profileGuid: ID to use to lookup profile settings for this connection // - settings: the TerminalSettings object to use to create the TerminalControl with. - void TerminalPage::_CreateNewTabFromSettings(GUID profileGuid, TerminalSettings settings) + // - existingConnection: optionally receives a connection from the outside world instead of attempting to create one + void TerminalPage::_CreateNewTabFromSettings(GUID profileGuid, TerminalSettings settings, TerminalConnection::ITerminalConnection existingConnection) { // Initialize the new tab - - // Create a connection based on the values in our settings object. - auto connection = _CreateConnectionFromSettings(profileGuid, settings); + // Create a connection based on the values in our settings object if we weren't given one. + auto connection = existingConnection ? existingConnection : _CreateConnectionFromSettings(profileGuid, settings); TerminalConnection::ITerminalConnection debugConnection{ nullptr }; if (_settings.GlobalSettings().DebugFeaturesEnabled()) @@ -2723,6 +2739,19 @@ namespace winrt::TerminalApp::implementation _startupActions = winrt::single_threaded_vector(std::move(listCopy)); } + // Routine Description: + // - Notifies this Terminal Page that it should start the incoming connection + // listener for command-line tools attempting to join this Terminal + // through the default application channel. + // Arguments: + // - - Implicitly sets to true. Default page state is false. + // Return Value: + // - + void TerminalPage::SetInboundListener() + { + _shouldStartInboundListener = true; + } + winrt::TerminalApp::IDialogPresenter TerminalPage::DialogPresenter() const { return _dialogPresenter.get(); @@ -3033,6 +3062,7 @@ namespace winrt::TerminalApp::implementation { return _isFullscreen; } + // Method Description: // - Returns true if we're currently in "Always on top" mode. When we're in // always on top mode, the window should be on top of all other windows. @@ -3047,6 +3077,12 @@ namespace winrt::TerminalApp::implementation return _isAlwaysOnTop; } + void TerminalPage::_OnNewConnection(winrt::Microsoft::Terminal::TerminalConnection::ITerminalConnection connection) + { + // TODO: GH 9458 will give us more context so we can try to choose a better profile. + _OpenNewTab(nullptr, connection); + } + // Method Description: // - Updates all tabs with their current index in _tabs. // Arguments: @@ -3155,7 +3191,6 @@ namespace winrt::TerminalApp::implementation } } - // Method Description: // Method Description: // - Computes the delta for scrolling the tab's viewport. // Arguments: diff --git a/src/cascadia/TerminalApp/TerminalPage.h b/src/cascadia/TerminalApp/TerminalPage.h index 2f6348a4f..bb830b977 100644 --- a/src/cascadia/TerminalApp/TerminalPage.h +++ b/src/cascadia/TerminalApp/TerminalPage.h @@ -66,6 +66,7 @@ namespace winrt::TerminalApp::implementation bool AlwaysOnTop() const; void SetStartupActions(std::vector& actions); + void SetInboundListener(); static std::vector ConvertExecuteCommandlineToActions(const Microsoft::Terminal::Settings::Model::ExecuteCommandlineArgs& args); winrt::TerminalApp::IDialogPresenter DialogPresenter() const; @@ -138,6 +139,7 @@ namespace winrt::TerminalApp::implementation StartupState _startupState{ StartupState::NotInitialized }; Windows::Foundation::Collections::IVector _startupActions; + bool _shouldStartInboundListener{ false }; void _ShowAboutDialog(); winrt::Windows::Foundation::IAsyncOperation _ShowCloseWarningDialog(); @@ -147,8 +149,8 @@ namespace winrt::TerminalApp::implementation void _CreateNewTabFlyout(); void _OpenNewTabDropdown(); - void _OpenNewTab(const Microsoft::Terminal::Settings::Model::NewTerminalArgs& newTerminalArgs); - void _CreateNewTabFromSettings(GUID profileGuid, Microsoft::Terminal::Settings::Model::TerminalSettings settings); + void _OpenNewTab(const Microsoft::Terminal::Settings::Model::NewTerminalArgs& newTerminalArgs, winrt::Microsoft::Terminal::TerminalConnection::ITerminalConnection existingConnection = nullptr); + void _CreateNewTabFromSettings(GUID profileGuid, Microsoft::Terminal::Settings::Model::TerminalSettings settings, winrt::Microsoft::Terminal::TerminalConnection::ITerminalConnection existingConnection = nullptr); winrt::Microsoft::Terminal::TerminalConnection::ITerminalConnection _CreateConnectionFromSettings(GUID profileGuid, Microsoft::Terminal::Settings::Model::TerminalSettings settings); winrt::fire_and_forget _OpenNewWindow(const bool elevate, const Microsoft::Terminal::Settings::Model::NewTerminalArgs newTerminalArgs); @@ -275,6 +277,8 @@ namespace winrt::TerminalApp::implementation void _HidePointerCursorHandler(const IInspectable& sender, const IInspectable& eventArgs); void _RestorePointerCursorHandler(const IInspectable& sender, const IInspectable& eventArgs); + void _OnNewConnection(winrt::Microsoft::Terminal::TerminalConnection::ITerminalConnection connection); + #pragma region ActionHandlers // These are all defined in AppActionHandlers.cpp void _HandleOpenNewTabDropdown(const IInspectable& sender, const Microsoft::Terminal::Settings::Model::ActionEventArgs& args); @@ -322,6 +326,7 @@ namespace winrt::TerminalApp::implementation void _HandleFindMatch(const IInspectable& sender, const Microsoft::Terminal::Settings::Model::ActionEventArgs& args); void _HandleTogglePaneReadOnly(const IInspectable& sender, const Microsoft::Terminal::Settings::Model::ActionEventArgs& args); void _HandleNewWindow(const IInspectable& sender, const Microsoft::Terminal::Settings::Model::ActionEventArgs& args); + void _HandleToggleInboundPty(const IInspectable& sender, const Microsoft::Terminal::Settings::Model::ActionEventArgs& args); // Make sure to hook new actions up in _RegisterActionCallbacks! #pragma endregion diff --git a/src/cascadia/TerminalConnection/CTerminalHandoff.cpp b/src/cascadia/TerminalConnection/CTerminalHandoff.cpp new file mode 100644 index 000000000..276111903 --- /dev/null +++ b/src/cascadia/TerminalConnection/CTerminalHandoff.cpp @@ -0,0 +1,108 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +#include "pch.h" + +#include "CTerminalHandoff.h" + +using namespace Microsoft::WRL; + +// The callback function when a connection is received +static NewHandoffFunction _pfnHandoff = nullptr; +// The registration ID of the class object for clean up later +static DWORD g_cTerminalHandoffRegistration = 0; + +// Routine Description: +// - Starts listening for TerminalHandoff requests by registering +// our class and interface with COM. +// Arguments: +// - pfnHandoff - Function to callback when a handoff is received +// Return Value: +// - S_OK, E_NOT_VALID_STATE (start called when already started) or relevant COM registration error. +HRESULT CTerminalHandoff::s_StartListening(NewHandoffFunction pfnHandoff) noexcept +try +{ + RETURN_HR_IF(E_NOT_VALID_STATE, _pfnHandoff != nullptr); + + const auto classFactory = Make>(); + + RETURN_IF_NULL_ALLOC(classFactory); + + ComPtr unk; + RETURN_IF_FAILED(classFactory.As(&unk)); + + RETURN_IF_FAILED(CoRegisterClassObject(__uuidof(CTerminalHandoff), unk.Get(), CLSCTX_LOCAL_SERVER, REGCLS_MULTIPLEUSE, &g_cTerminalHandoffRegistration)); + + _pfnHandoff = pfnHandoff; + + return S_OK; +} +CATCH_RETURN() + +// Routine Description: +// - Stops listening for TerminalHandoff requests by revoking the registration +// our class and interface with COM +// Arguments: +// - +// Return Value: +// - S_OK, E_NOT_VALID_STATE (stop called when not started), or relevant COM class revoke error +HRESULT CTerminalHandoff::s_StopListening() noexcept +{ + RETURN_HR_IF_NULL(E_NOT_VALID_STATE, _pfnHandoff); + + _pfnHandoff = nullptr; + + if (g_cTerminalHandoffRegistration) + { + RETURN_IF_FAILED(CoRevokeClassObject(g_cTerminalHandoffRegistration)); + g_cTerminalHandoffRegistration = 0; + } + + return S_OK; +} + +// Routine Description: +// - Helper to duplicate a handle to ourselves so we can keep holding onto it +// after the caller frees the original one. +// Arguments: +// - in - Handle to duplicate +// - out - Where to place the duplicated value +// Return Value: +// - S_OK or Win32 error from `::DuplicateHandle` +static HRESULT _duplicateHandle(const HANDLE in, HANDLE& out) noexcept +{ + RETURN_IF_WIN32_BOOL_FALSE(::DuplicateHandle(GetCurrentProcess(), in, GetCurrentProcess(), &out, 0, FALSE, DUPLICATE_SAME_ACCESS)); + return S_OK; +} + +// Routine Description: +// - Receives the terminal handoff via COM from the other process, +// duplicates handles as COM will free those given on the way out, +// then fires off an event notifying the rest of the terminal that +// a connection is on its way in. +// Arguments: +// - in - PTY input handle that we will read from +// - out - PTY output handle that we will write to +// - signal - PTY signal handle for out of band messaging +// - process - Process handle to client so we can track its lifetime and exit appropriately +// Return Value: +// - E_NOT_VALID_STATE if a event handler is not registered before calling. `::DuplicateHandle` +// error codes if we cannot manage to make our own copy of handles to retain. Or S_OK/error +// from the registered handler event function. +HRESULT CTerminalHandoff::EstablishPtyHandoff(HANDLE in, HANDLE out, HANDLE signal, HANDLE process) noexcept +{ + // Report an error if no one registered a handoff function before calling this. + RETURN_HR_IF_NULL(E_NOT_VALID_STATE, _pfnHandoff); + + // Duplicate the handles from what we received. + // The contract with COM specifies that any HANDLEs we receive from the caller belong + // to the caller and will be freed when we leave the scope of this method. + // Making our own duplicate copy ensures they hang around in our lifetime. + RETURN_IF_FAILED(_duplicateHandle(in, in)); + RETURN_IF_FAILED(_duplicateHandle(out, out)); + RETURN_IF_FAILED(_duplicateHandle(signal, signal)); + RETURN_IF_FAILED(_duplicateHandle(process, process)); + + // Call registered handler from when we started listening. + return _pfnHandoff(in, out, signal, process); +} diff --git a/src/cascadia/TerminalConnection/CTerminalHandoff.h b/src/cascadia/TerminalConnection/CTerminalHandoff.h new file mode 100644 index 000000000..396f4a095 --- /dev/null +++ b/src/cascadia/TerminalConnection/CTerminalHandoff.h @@ -0,0 +1,56 @@ +/*++ +Copyright (c) Microsoft Corporation +Licensed under the MIT license. + +Module Name: +- CTerminalHandoff.h + +Abstract: +- This module receives an incoming request to host a terminal UX + for a console mode application already started and attached to a PTY. + +Author(s): +- Michael Niksa (MiNiksa) 31-Aug-2020 + +--*/ + +#pragma once + +#include "ITerminalHandoff.h" + +#if defined(WT_BRANDING_RELEASE) +#define __CLSID_CTerminalHandoff "E12CFF52-A866-4C77-9A90-F570A7AA2C6B" +#elif defined(WT_BRANDING_PREVIEW) +#define __CLSID_CTerminalHandoff "86633F1F-6454-40EC-89CE-DA4EBA977EE2" +#else +#define __CLSID_CTerminalHandoff "051F34EE-C1FD-4B19-AF75-9BA54648434C" +#endif + +using NewHandoffFunction = HRESULT (*)(HANDLE, HANDLE, HANDLE, HANDLE); + +struct __declspec(uuid(__CLSID_CTerminalHandoff)) + CTerminalHandoff : public Microsoft::WRL::RuntimeClass, ITerminalHandoff> +{ +#pragma region ITerminalHandoff + STDMETHODIMP EstablishPtyHandoff(HANDLE in, + HANDLE out, + HANDLE signal, + HANDLE process) noexcept override; + +#pragma endregion + + static HRESULT s_StartListening(NewHandoffFunction pfnHandoff) noexcept; + static HRESULT s_StopListening() noexcept; +}; + +// Disable warnings from the CoCreatableClass macro as the value it provides for +// automatic COM class registration is of much greater value than the nits from +// the static analysis warnings. +#pragma warning(push) + +#pragma warning(disable : 26477) // Macro uses 0/NULL over nullptr. +#pragma warning(disable : 26476) // Macro uses naked union over variant. + +CoCreatableClass(CTerminalHandoff); + +#pragma warning(pop) diff --git a/src/cascadia/TerminalConnection/ConptyConnection.cpp b/src/cascadia/TerminalConnection/ConptyConnection.cpp index 1b96cc92c..f7e5b7e81 100644 --- a/src/cascadia/TerminalConnection/ConptyConnection.cpp +++ b/src/cascadia/TerminalConnection/ConptyConnection.cpp @@ -9,6 +9,7 @@ #include #include "ConptyConnection.g.cpp" +#include "CTerminalHandoff.h" #include "../../types/inc/utils.hpp" #include "../../types/inc/Environment.hpp" @@ -190,6 +191,32 @@ namespace winrt::Microsoft::Terminal::TerminalConnection::implementation } CATCH_RETURN(); + ConptyConnection::ConptyConnection(const HANDLE hSig, + const HANDLE hIn, + const HANDLE hOut, + const HANDLE hClientProcess) : + _initialRows{ 25 }, + _initialCols{ 80 }, + _commandline{ L"" }, + _startingDirectory{ L"" }, + _startingTitle{ L"" }, + _environment{ nullptr }, + _guid{}, + _u8State{}, + _u16Str{}, + _buffer{}, + _inPipe{ hIn }, + _outPipe{ hOut } + { + hSig; // TODO: GH 9464 this needs to be packed into the hpcon + if (_guid == guid{}) + { + _guid = Utils::CreateGuid(); + } + + _piClient.hProcess = hClientProcess; + } + ConptyConnection::ConptyConnection(const hstring& commandline, const hstring& startingDirectory, const hstring& startingTitle, @@ -222,9 +249,12 @@ namespace winrt::Microsoft::Terminal::TerminalConnection::implementation void ConptyConnection::Start() try { - const COORD dimensions{ gsl::narrow_cast(_initialCols), gsl::narrow_cast(_initialRows) }; - THROW_IF_FAILED(_CreatePseudoConsoleAndPipes(dimensions, PSEUDOCONSOLE_RESIZE_QUIRK | PSEUDOCONSOLE_WIN32_INPUT_MODE, &_inPipe, &_outPipe, &_hPC)); - THROW_IF_FAILED(_LaunchAttachedClient()); + if (!_inPipe) + { + const COORD dimensions{ gsl::narrow_cast(_initialCols), gsl::narrow_cast(_initialRows) }; + THROW_IF_FAILED(_CreatePseudoConsoleAndPipes(dimensions, PSEUDOCONSOLE_RESIZE_QUIRK | PSEUDOCONSOLE_WIN32_INPUT_MODE, &_inPipe, &_outPipe, &_hPC)); + THROW_IF_FAILED(_LaunchAttachedClient()); + } _startTime = std::chrono::high_resolution_clock::now(); @@ -451,6 +481,31 @@ namespace winrt::Microsoft::Terminal::TerminalConnection::implementation return 0; } + static winrt::event _newConnectionHandlers; + + winrt::event_token ConptyConnection::NewConnection(NewConnectionHandler const& handler) { return _newConnectionHandlers.add(handler); }; + void ConptyConnection::NewConnection(winrt::event_token const& token) { _newConnectionHandlers.remove(token); }; + + HRESULT ConptyConnection::NewHandoff(HANDLE in, HANDLE out, HANDLE signal, HANDLE process) noexcept + try + { + auto conn = winrt::make(signal, in, out, process); + _newConnectionHandlers(conn); + + return S_OK; + } + CATCH_RETURN() + + void ConptyConnection::StartInboundListener() + { + THROW_IF_FAILED(CTerminalHandoff::s_StartListening(&ConptyConnection::NewHandoff)); + } + + void ConptyConnection::StopInboundListener() + { + THROW_IF_FAILED(CTerminalHandoff::s_StopListening()); + } + // Function Description: // - This function will be called (by C++/WinRT) after the final outstanding reference to // any given connection instance is released. diff --git a/src/cascadia/TerminalConnection/ConptyConnection.h b/src/cascadia/TerminalConnection/ConptyConnection.h index 557fb4969..3eee9fc8d 100644 --- a/src/cascadia/TerminalConnection/ConptyConnection.h +++ b/src/cascadia/TerminalConnection/ConptyConnection.h @@ -19,6 +19,11 @@ namespace winrt::Microsoft::Terminal::TerminalConnection::implementation { struct ConptyConnection : ConptyConnectionT, ConnectionStateHolder { + ConptyConnection(const HANDLE hSig, + const HANDLE hIn, + const HANDLE hOut, + const HANDLE hClientProcess); + ConptyConnection( const hstring& cmdline, const hstring& startingDirectory, @@ -36,6 +41,12 @@ namespace winrt::Microsoft::Terminal::TerminalConnection::implementation winrt::guid Guid() const noexcept; + static void StartInboundListener(); + static void StopInboundListener(); + + static winrt::event_token NewConnection(NewConnectionHandler const& handler); + static void NewConnection(winrt::event_token const& token); + WINRT_CALLBACK(TerminalOutput, TerminalOutputHandler); private: @@ -43,6 +54,8 @@ namespace winrt::Microsoft::Terminal::TerminalConnection::implementation void _indicateExitWithStatus(unsigned int status) noexcept; void _ClientTerminated() noexcept; + static HRESULT NewHandoff(HANDLE in, HANDLE out, HANDLE signal, HANDLE process) noexcept; + uint32_t _initialRows{}; uint32_t _initialCols{}; hstring _commandline; diff --git a/src/cascadia/TerminalConnection/ConptyConnection.idl b/src/cascadia/TerminalConnection/ConptyConnection.idl index fc53f368b..766c7263a 100644 --- a/src/cascadia/TerminalConnection/ConptyConnection.idl +++ b/src/cascadia/TerminalConnection/ConptyConnection.idl @@ -5,11 +5,13 @@ import "ITerminalConnection.idl"; namespace Microsoft.Terminal.TerminalConnection { - [default_interface] - runtimeclass ConptyConnection : ITerminalConnection + [default_interface] runtimeclass ConptyConnection : ITerminalConnection { - ConptyConnection(String cmdline, String startingDirectory, String startingTitle, IMapView environment, UInt32 rows, UInt32 columns, Guid guid); + ConptyConnection(String cmdline, String startingDirectory, String startingTitle, IMapView environment, UInt32 rows, UInt32 columns, Guid guid); Guid Guid { get; }; - }; + static event NewConnectionHandler NewConnection; + static void StartInboundListener(); + static void StopInboundListener(); + }; } diff --git a/src/cascadia/TerminalConnection/ITerminalConnection.idl b/src/cascadia/TerminalConnection/ITerminalConnection.idl index 398724cd8..e485cc06d 100644 --- a/src/cascadia/TerminalConnection/ITerminalConnection.idl +++ b/src/cascadia/TerminalConnection/ITerminalConnection.idl @@ -27,4 +27,6 @@ namespace Microsoft.Terminal.TerminalConnection event Windows.Foundation.TypedEventHandler StateChanged; ConnectionState State { get; }; }; + + delegate void NewConnectionHandler(ITerminalConnection connection); } diff --git a/src/cascadia/TerminalConnection/TerminalConnection.vcxproj b/src/cascadia/TerminalConnection/TerminalConnection.vcxproj index 958593eae..625484523 100644 --- a/src/cascadia/TerminalConnection/TerminalConnection.vcxproj +++ b/src/cascadia/TerminalConnection/TerminalConnection.vcxproj @@ -15,6 +15,7 @@ AzureConnection.idl + ConptyConnection.idl @@ -24,6 +25,7 @@ + AzureConnection.idl @@ -77,9 +79,12 @@ + + $(IntDir)..\OpenConsoleProxy;%(AdditionalIncludeDirectories) + $(OpenConsoleCommonOutDir)\conptylib.lib;%(AdditionalDependencies) - + \ No newline at end of file diff --git a/src/cascadia/TerminalConnection/TerminalConnection.vcxproj.filters b/src/cascadia/TerminalConnection/TerminalConnection.vcxproj.filters index 91e6af3f5..12a9cc6c4 100644 --- a/src/cascadia/TerminalConnection/TerminalConnection.vcxproj.filters +++ b/src/cascadia/TerminalConnection/TerminalConnection.vcxproj.filters @@ -18,12 +18,14 @@ + + @@ -38,8 +40,6 @@ - - Resources\en-US - + \ No newline at end of file diff --git a/src/cascadia/TerminalSettingsEditor/Launch.cpp b/src/cascadia/TerminalSettingsEditor/Launch.cpp index b2bbaedd4..3b86bb3d3 100644 --- a/src/cascadia/TerminalSettingsEditor/Launch.cpp +++ b/src/cascadia/TerminalSettingsEditor/Launch.cpp @@ -37,4 +37,21 @@ namespace winrt::Microsoft::Terminal::Settings::Editor::implementation const auto profile{ winrt::unbox_value(value) }; _State.Settings().GlobalSettings().DefaultProfile(profile.Guid()); } + + // TODO GH#9463 - Complete hookup of Terminal UX to choose defapp. + Windows::Foundation::Collections::IObservableVector Launch::DefaultTerminals() + { + Windows::Foundation::Collections::IObservableVector vec; + return vec; + } + + IInspectable Launch::CurrentDefaultTerminal() + { + return nullptr; + } + + void Launch::CurrentDefaultTerminal(const IInspectable& value) + { + value; + } } diff --git a/src/cascadia/TerminalSettingsEditor/Launch.h b/src/cascadia/TerminalSettingsEditor/Launch.h index 8219939e0..3009e2c36 100644 --- a/src/cascadia/TerminalSettingsEditor/Launch.h +++ b/src/cascadia/TerminalSettingsEditor/Launch.h @@ -28,6 +28,10 @@ namespace winrt::Microsoft::Terminal::Settings::Editor::implementation IInspectable CurrentDefaultProfile(); void CurrentDefaultProfile(const IInspectable& value); + Windows::Foundation::Collections::IObservableVector DefaultTerminals(); + IInspectable CurrentDefaultTerminal(); + void CurrentDefaultTerminal(const IInspectable& value); + WINRT_PROPERTY(Editor::LaunchPageNavigationState, State, nullptr); GETSET_BINDABLE_ENUM_SETTING(LaunchMode, Model::LaunchMode, State().Settings().GlobalSettings, LaunchMode); diff --git a/src/cascadia/TerminalSettingsEditor/Launch.idl b/src/cascadia/TerminalSettingsEditor/Launch.idl index e2a1382e0..62e6ba929 100644 --- a/src/cascadia/TerminalSettingsEditor/Launch.idl +++ b/src/cascadia/TerminalSettingsEditor/Launch.idl @@ -17,10 +17,12 @@ namespace Microsoft.Terminal.Settings.Editor IInspectable CurrentDefaultProfile; + IInspectable CurrentDefaultTerminal; + IObservableVector DefaultTerminals { get; }; + IInspectable CurrentLaunchMode; IObservableVector LaunchModeList { get; }; - IInspectable CurrentWindowingBehavior; IObservableVector WindowingBehaviorList { get; }; } diff --git a/src/cascadia/TerminalSettingsEditor/Launch.xaml b/src/cascadia/TerminalSettingsEditor/Launch.xaml index ab0b60d18..eed1b6039 100644 --- a/src/cascadia/TerminalSettingsEditor/Launch.xaml +++ b/src/cascadia/TerminalSettingsEditor/Launch.xaml @@ -67,6 +67,16 @@ the MIT License. See LICENSE in the project root for license information. --> + + + + + + diff --git a/src/cascadia/TerminalSettingsEditor/Resources/en-US/Resources.resw b/src/cascadia/TerminalSettingsEditor/Resources/en-US/Resources.resw index c5b305d94..505d56fbc 100644 --- a/src/cascadia/TerminalSettingsEditor/Resources/en-US/Resources.resw +++ b/src/cascadia/TerminalSettingsEditor/Resources/en-US/Resources.resw @@ -221,6 +221,14 @@ Profile that opens when clicking the '+' icon or by typing the new tab key binding. A description for what the default profile is and when it's used. + + Default terminal + Header for a drop down that permits the user to select which installed Terminal application will launch when command line tools like CMD are run from the Windows Explorer run box or start menu or anywhere else that they do not already have a graphical window assigned. + + + The terminal application that launches when a command-line application is run without an existing session, like from the Start Menu or Run dialog. + A description to clarify that the dropdown choice for default terminal will tell the operating system which Terminal application (Windows Terminal, the preview build, the legacy inbox window, or a 3rd party one) to use when starting a command line tool like CMD or Powershell that does not already have a window. + Redraw entire screen when display updates Header for a control to toggle the "force full repaint" setting. When enabled, the app renders new content between screen frames. diff --git a/src/host/ConsoleArguments.cpp b/src/host/ConsoleArguments.cpp index 41c07c79b..7bd34b839 100644 --- a/src/host/ConsoleArguments.cpp +++ b/src/host/ConsoleArguments.cpp @@ -22,6 +22,7 @@ const std::wstring_view ConsoleArguments::RESIZE_QUIRK = L"--resizeQuirk"; const std::wstring_view ConsoleArguments::WIN32_INPUT_MODE = L"--win32input"; const std::wstring_view ConsoleArguments::FEATURE_ARG = L"--feature"; const std::wstring_view ConsoleArguments::FEATURE_PTY_ARG = L"pty"; +const std::wstring_view ConsoleArguments::COM_SERVER_ARG = L"-Embedding"; std::wstring EscapeArgument(std::wstring_view ac) { @@ -109,6 +110,7 @@ ConsoleArguments::ConsoleArguments(const std::wstring& commandline, _clientCommandline = L""; _vtMode = L""; _headless = false; + _runAsComServer = false; _createServerHandle = true; _serverHandle = 0; _signalHandle = 0; @@ -141,6 +143,7 @@ ConsoleArguments& ConsoleArguments::operator=(const ConsoleArguments& other) _height = other._height; _inheritCursor = other._inheritCursor; _receivedEarlySizeChange = other._receivedEarlySizeChange; + _runAsComServer = other._runAsComServer; } return *this; @@ -446,6 +449,12 @@ void ConsoleArguments::s_ConsumeArg(_Inout_ std::vector& args, _In s_ConsumeArg(args, i); hr = S_OK; } + else if (arg == COM_SERVER_ARG) + { + _runAsComServer = true; + s_ConsumeArg(args, i); + hr = S_OK; + } else if (arg.substr(0, FILEPATH_LEADER_PREFIX.length()) == FILEPATH_LEADER_PREFIX) { // beginning of command line -- includes file path @@ -576,6 +585,11 @@ bool ConsoleArguments::ShouldCreateServerHandle() const return _createServerHandle; } +bool ConsoleArguments::ShouldRunAsComServer() const +{ + return _runAsComServer; +} + HANDLE ConsoleArguments::GetServerHandle() const { return ULongToHandle(_serverHandle); @@ -596,6 +610,11 @@ HANDLE ConsoleArguments::GetVtOutHandle() const return _vtOutHandle; } +std::wstring ConsoleArguments::GetOriginalCommandLine() const +{ + return _commandline; +} + std::wstring ConsoleArguments::GetClientCommandline() const { return _clientCommandline; diff --git a/src/host/ConsoleArguments.hpp b/src/host/ConsoleArguments.hpp index e2e7cad2d..86c43be05 100644 --- a/src/host/ConsoleArguments.hpp +++ b/src/host/ConsoleArguments.hpp @@ -35,6 +35,7 @@ public: bool InConptyMode() const noexcept; bool IsHeadless() const; bool ShouldCreateServerHandle() const; + bool ShouldRunAsComServer() const; HANDLE GetServerHandle() const; HANDLE GetVtInHandle() const; @@ -43,6 +44,7 @@ public: bool HasSignalHandle() const; HANDLE GetSignalHandle() const; + std::wstring GetOriginalCommandLine() const; std::wstring GetClientCommandline() const; std::wstring GetVtMode() const; bool GetForceV1() const; @@ -74,6 +76,7 @@ public: static const std::wstring_view WIN32_INPUT_MODE; static const std::wstring_view FEATURE_ARG; static const std::wstring_view FEATURE_PTY_ARG; + static const std::wstring_view COM_SERVER_ARG; private: #ifdef UNIT_TESTING @@ -90,7 +93,8 @@ private: const bool createServerHandle, const DWORD serverHandle, const DWORD signalHandle, - const bool inheritCursor) : + const bool inheritCursor, + const bool runAsComServer) : _commandline(commandline), _clientCommandline(clientCommandline), _vtInHandle(vtInHandle), @@ -107,7 +111,8 @@ private: _resizeQuirk(false), _receivedEarlySizeChange{ false }, _originalWidth{ -1 }, - _originalHeight{ -1 } + _originalHeight{ -1 }, + _runAsComServer{ runAsComServer } { } #endif @@ -128,6 +133,7 @@ private: short _width; short _height; + bool _runAsComServer; bool _createServerHandle; DWORD _serverHandle; DWORD _signalHandle; @@ -186,6 +192,7 @@ namespace WEX L"Use Signal Handle: '%ws'\r\n" L"Signal Handle: '0x%x'\r\n", L"Inherit Cursor: '%ws'\r\n", + L"Run As Com Server: '%ws'\r\n", ci.GetClientCommandline().c_str(), s_ToBoolString(ci.HasVtHandles()), ci.GetVtInHandle(), @@ -199,7 +206,8 @@ namespace WEX ci.GetServerHandle(), s_ToBoolString(ci.HasSignalHandle()), ci.GetSignalHandle(), - s_ToBoolString(ci.GetInheritCursor())); + s_ToBoolString(ci.GetInheritCursor()), + s_ToBoolString(ci.ShouldRunAsComServer())); } private: diff --git a/src/host/exe/CConsoleHandoff.cpp b/src/host/exe/CConsoleHandoff.cpp new file mode 100644 index 000000000..20ac3c6e8 --- /dev/null +++ b/src/host/exe/CConsoleHandoff.cpp @@ -0,0 +1,62 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +#include "precomp.h" + +#include "CConsoleHandoff.h" + +#include "srvinit.h" + +// Routine Description: +// - Helper to duplicate a handle to ourselves so we can keep holding onto it +// after the caller frees the original one. +// Arguments: +// - in - Handle to duplicate +// - out - Where to place the duplicated value +// Return Value: +// - S_OK or Win32 error from `::DuplicateHandle` +static HRESULT _duplicateHandle(const HANDLE in, HANDLE& out) +{ + RETURN_IF_WIN32_BOOL_FALSE(DuplicateHandle(GetCurrentProcess(), in, GetCurrentProcess(), &out, 0, FALSE, DUPLICATE_SAME_ACCESS)); + return S_OK; +} + +// Routine Description: +// - Takes the incoming information from COM and and prepares a console hosting session in this process. +// Arguments: +// - server - Console driver server handle +// - inputEvent - Event already established that we signal when new input data is available in case the driver is waiting on us +// - msg - Portable attach message containing just enough descriptor payload to get us started in servicing it +HRESULT CConsoleHandoff::EstablishHandoff(HANDLE server, + HANDLE inputEvent, + PCCONSOLE_PORTABLE_ATTACH_MSG msg) +try +{ + // Fill the descriptor portion of a fresh api message with the received data. + // The descriptor portion is the "received" packet from the last ask of the driver. + // The other portions are unnecessary as they track the other buffer state, error codes, + // and the return portion of the api message. + // We will re-retrieve the connect information (title, window state, etc.) when the + // new console session begins servicing this. + CONSOLE_API_MSG apiMsg{}; + apiMsg.Descriptor.Identifier.HighPart = msg->IdHighPart; + apiMsg.Descriptor.Identifier.LowPart = msg->IdLowPart; + apiMsg.Descriptor.Process = static_cast(msg->Process); + apiMsg.Descriptor.Object = static_cast(msg->Object); + apiMsg.Descriptor.Function = msg->Function; + apiMsg.Descriptor.InputSize = msg->InputSize; + apiMsg.Descriptor.OutputSize = msg->OutputSize; + + // Duplicate the handles from what we received. + // The contract with COM specifies that any HANDLEs we receive from the caller belong + // to the caller and will be freed when we leave the scope of this method. + // Making our own duplicate copy ensures they hang around in our lifetime. + RETURN_IF_FAILED(_duplicateHandle(server, server)); + RETURN_IF_FAILED(_duplicateHandle(inputEvent, inputEvent)); + + // Now perform the handoff. + RETURN_IF_FAILED(ConsoleEstablishHandoff(server, inputEvent, &apiMsg)); + + return S_OK; +} +CATCH_RETURN(); diff --git a/src/host/exe/CConsoleHandoff.h b/src/host/exe/CConsoleHandoff.h new file mode 100644 index 000000000..e2d2e14d8 --- /dev/null +++ b/src/host/exe/CConsoleHandoff.h @@ -0,0 +1,42 @@ +/*++ +Copyright (c) Microsoft Corporation +Licensed under the MIT license. + +Module Name: +- CConsoleHandoff.h + +Abstract: +- This module receives a console session handoff from the operating system to + an out-of-band, out-of-box console host. + +Author(s): +- Michael Niksa (MiNiksa) 31-Aug-2020 + +--*/ + +#pragma once + +#include "IConsoleHandoff.h" + +#if defined(WT_BRANDING_RELEASE) +#define __CLSID_CConsoleHandoff "2EACA947-7F5F-4CFA-BA87-8F7FBEEFBE69" +#elif defined(WT_BRANDING_PREVIEW) +#define __CLSID_CConsoleHandoff "06EC847C-C0A5-46B8-92CB-7C92F6E35CD5" +#else +#define __CLSID_CConsoleHandoff "1F9F2BF5-5BC3-4F17-B0E6-912413F1F451" +#endif + +using namespace Microsoft::WRL; + +struct __declspec(uuid(__CLSID_CConsoleHandoff)) + CConsoleHandoff : public RuntimeClass, IConsoleHandoff> +{ +#pragma region IConsoleHandoff + STDMETHODIMP EstablishHandoff(HANDLE server, + HANDLE inputEvent, + PCCONSOLE_PORTABLE_ATTACH_MSG msg); + +#pragma endregion +}; + +CoCreatableClass(CConsoleHandoff); diff --git a/src/host/exe/Host.EXE.vcxproj b/src/host/exe/Host.EXE.vcxproj index 42bafc2ab..c43679ab6 100644 --- a/src/host/exe/Host.EXE.vcxproj +++ b/src/host/exe/Host.EXE.vcxproj @@ -8,9 +8,10 @@ OpenConsole Application - + + @@ -18,7 +19,8 @@ Create ProgramDatabase - + + @@ -80,7 +82,7 @@ - ..;%(AdditionalIncludeDirectories) + ..;$(IntDir)..\OpenConsoleProxy;%(AdditionalIncludeDirectories) true diff --git a/src/host/exe/Host.EXE.vcxproj.filters b/src/host/exe/Host.EXE.vcxproj.filters index 2c339a909..3b33223f7 100644 --- a/src/host/exe/Host.EXE.vcxproj.filters +++ b/src/host/exe/Host.EXE.vcxproj.filters @@ -21,12 +21,18 @@ Header Files + + Header Files + Source Files - + + Source Files + + Source Files diff --git a/src/host/exe/exemain.cpp b/src/host/exe/exemain.cpp new file mode 100644 index 000000000..d955edfa8 --- /dev/null +++ b/src/host/exe/exemain.cpp @@ -0,0 +1,334 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +#include "precomp.h" + +#include "ConsoleArguments.hpp" +#include "srvinit.h" +#include "CConsoleHandoff.h" +#include "../server/Entrypoints.h" +#include "../interactivity/inc/ServiceLocator.hpp" +#include "../inc/conint.h" + +// Define TraceLogging provider +TRACELOGGING_DEFINE_PROVIDER( + g_ConhostLauncherProvider, + "Microsoft.Windows.Console.Launcher", + // {770aa552-671a-5e97-579b-151709ec0dbd} + (0x770aa552, 0x671a, 0x5e97, 0x57, 0x9b, 0x15, 0x17, 0x09, 0xec, 0x0d, 0xbd), + TraceLoggingOptionMicrosoftTelemetry()); + +// Define a specialization of WRL::Module so we can specify a REGCLS_SINGLEUSE type server. +// We would like to use all the conveniences afforded to us by WRL::Module, but it only +// creates REGCLS_MULTIPLEUSE with no override. This makes an override for it by taking advantage +// of its existing virtual declarations. +#pragma region Single Use Out of Proc Specialization +template +class DefaultOutOfProcModuleWithRegistrationFlag; + +template> +class OutOfProcModuleWithRegistrationFlag : public Microsoft::WRL::Module +{ + using Elsewhere = Module; + using Super = Details::OutOfProcModuleBase; + +public: + STDMETHOD(RegisterCOMObject) + (_In_opt_z_ const wchar_t* serverName, _In_reads_(count) IID* clsids, _In_reads_(count) IClassFactory** factories, _Inout_updates_(count) DWORD* cookies, unsigned int count) + { + return Microsoft::WRL::Details::RegisterCOMObject(serverName, clsids, factories, cookies, count); + } +}; + +template +class DefaultOutOfProcModuleWithRegistrationFlag : public OutOfProcModuleWithRegistrationFlag> +{ +}; +#pragma endregion + +// Holds the wwinmain open until COM tells us there are no more server connections +wil::unique_event _comServerExitEvent; + +static bool ConhostV2ForcedInRegistry() +{ + // If the registry value doesn't exist, or exists and is non-zero, we should default to using the v2 console. + // Otherwise, in the case of an explicit value of 0, we should use the legacy console. + bool fShouldUseConhostV2 = true; + PCSTR pszErrorDescription = nullptr; + bool fIgnoreError = false; + + // open HKCU\Console + wil::unique_hkey hConsoleSubKey; + LONG lStatus = NTSTATUS_FROM_WIN32(RegOpenKeyExW(HKEY_CURRENT_USER, L"Console", 0, KEY_READ, &hConsoleSubKey)); + if (ERROR_SUCCESS == lStatus) + { + // now get the value of the ForceV2 reg value, if it exists + DWORD dwValue; + DWORD dwType; + DWORD cbValue = sizeof(dwValue); + lStatus = RegQueryValueExW(hConsoleSubKey.get(), + L"ForceV2", + nullptr, + &dwType, + (PBYTE)&dwValue, + &cbValue); + + if (ERROR_SUCCESS == lStatus && + dwType == REG_DWORD && // response is a DWORD + cbValue == sizeof(dwValue)) // response data exists + { + // Value exists. If non-zero use v2 console. + fShouldUseConhostV2 = dwValue != 0; + } + else + { + pszErrorDescription = "RegQueryValueKey Failed"; + fIgnoreError = lStatus == ERROR_FILE_NOT_FOUND; + } + } + else + { + pszErrorDescription = "RegOpenKey Failed"; + // ignore error caused by RegOpenKey if it's a simple case of the key not being found + fIgnoreError = lStatus == ERROR_FILE_NOT_FOUND; + } + + return fShouldUseConhostV2; +} + +[[nodiscard]] static HRESULT ValidateServerHandle(const HANDLE handle) +{ + // Make sure this is a console file. + FILE_FS_DEVICE_INFORMATION DeviceInformation; + IO_STATUS_BLOCK IoStatusBlock; + NTSTATUS const Status = NtQueryVolumeInformationFile(handle, &IoStatusBlock, &DeviceInformation, sizeof(DeviceInformation), FileFsDeviceInformation); + if (!NT_SUCCESS(Status)) + { + RETURN_NTSTATUS(Status); + } + else if (DeviceInformation.DeviceType != FILE_DEVICE_CONSOLE) + { + return E_INVALIDARG; + } + else + { + return S_OK; + } +} + +static bool ShouldUseLegacyConhost(const ConsoleArguments& args) +{ + if (args.InConptyMode()) + { + return false; + } + + if (args.GetForceV1()) + { + return true; + } + + // Per the documentation in ConhostV2ForcedInRegistry, it checks the value + // of HKCU\Console:ForceV2. If it's *not found* or nonzero, "v2" is forced. + return !ConhostV2ForcedInRegistry(); +} + +[[nodiscard]] static HRESULT ActivateLegacyConhost(const HANDLE handle) +{ + HRESULT hr = S_OK; + + // TraceLog that we're using the legacy console. We won't log new console + // because there's already a count of how many total processes were launched. + // Total - legacy = new console. + // We expect legacy launches to be infrequent enough to not cause an issue. + TraceLoggingWrite(g_ConhostLauncherProvider, "IsLegacyLoaded", TraceLoggingBool(true, "ConsoleLegacy"), TraceLoggingKeyword(MICROSOFT_KEYWORD_TELEMETRY)); + + const PCWSTR pszConhostDllName = L"ConhostV1.dll"; + + // Load our implementation, and then Load/Launch the IO thread. + wil::unique_hmodule hConhostBin(LoadLibraryExW(pszConhostDllName, nullptr, LOAD_LIBRARY_SEARCH_SYSTEM32)); + if (hConhostBin.get() != nullptr) + { + typedef NTSTATUS (*PFNCONSOLECREATEIOTHREAD)(__in HANDLE Server); + + PFNCONSOLECREATEIOTHREAD pfnConsoleCreateIoThread = (PFNCONSOLECREATEIOTHREAD)GetProcAddress(hConhostBin.get(), "ConsoleCreateIoThread"); + if (pfnConsoleCreateIoThread != nullptr) + { + hr = HRESULT_FROM_NT(pfnConsoleCreateIoThread(handle)); + } + else + { + hr = HRESULT_FROM_WIN32(GetLastError()); + } + } + else + { + // setup status error + hr = HRESULT_FROM_WIN32(GetLastError()); + } + + if (SUCCEEDED(hr)) + { + hConhostBin.release(); + } + + return hr; +} + +// Routine Description: +// - Called back when COM says there is nothing left for our server to do and we can tear down. +static void _releaseNotifier() noexcept +{ + _comServerExitEvent.SetEvent(); +} + +// Routine Description: +// - Main entry point for EXE version of console launching. +// This can be used as a debugging/diagnostics tool as well as a method of testing the console without +// replacing the system binary. +// Arguments: +// - hInstance - This module instance pointer is saved for resource lookups. +// - hPrevInstance - Unused pointer to the module instances. See wWinMain definitions @ MSDN for more details. +// - pwszCmdLine - Unused variable. We will look up the command line using GetCommandLineW(). +// - nCmdShow - Unused variable specifying window show/hide state for Win32 mode applications. +// Return value: +// - [[noreturn]] - This function will not return. It will kill the thread we were called from and the console server threads will take over. +int CALLBACK wWinMain( + _In_ HINSTANCE hInstance, + _In_ HINSTANCE /*hPrevInstance*/, + _In_ PWSTR /*pwszCmdLine*/, + _In_ int /*nCmdShow*/) +{ + Microsoft::Console::Interactivity::ServiceLocator::LocateGlobals().hInstance = hInstance; + + ConsoleCheckDebug(); + + // Set up OutOfProc COM server stuff in case we become one. + // WRL Module gets going right before winmain is called, so if we don't + // set this up appropriately... other things using WRL that aren't us + // could get messed up by the singleton module and cause unexpected errors. + _comServerExitEvent.create(); + + // We will use a single use server to ensure that each out-of-box console that + // gets activated to take over a session from the OS console will only be responsible + // for ONE console server session. This ensures that we, as the handoff target, are + // responsible for only one session and one server handle to the driver and we maintain + // the one-to-one relationship between console sessions and servers just like the inbox + // one. Theoretically we could combine them if we had any way of keeping track of all + // of the console state separately per server connection... but that's not how this is + // all designed (so many globals) and it would potentially risk one session's crash + // taking down some completely unrelated command-line clients. + // ---- + // The general flow is... + // 1. The in-box console looks up the registered delegation console + // 2. An OpenConsole.exe is typically found which is a newer version of the + // same code that is in-box and may have more bug fixes or features (especially + // an improved VT dialect or something of that ilk). + // 3. By activating the registered CLSID, the in-box console will be starting `openconsole.exe -Embedding` + // through the OutOfProc COM server infrastructure. + // 4. The `openconsole.exe -Embedding` that starts will come through right here and register + // `CConsoleHandoff` to accept ONE connection. + // 5. The in-box console will then receive an `IConsoleHandoff` to this `CConsoleHandoff` and the registration + // immediately expires, letting no one else in. The next caller will start another new `openconsole.exe -Embedding` process. + // 6. The in-box console invokes the handoff method on the interface and it transfers some data into `CConsoleHandoff` + // of the `OpenConsole.exe` which will then stand up its own server IO thread and handle all console server session + // messages going forward. + // 7. The out-of-box `OpenConsole.exe` can then attempt to lookup and invoke a `CTerminalHandoff` to ask a registered + // Terminal to become the UI. This OpenConsole.exe will put itself in PTY mode and let the Terminal handle user interaction. + auto& module = OutOfProcModuleWithRegistrationFlag::Create(&_releaseNotifier); + + // Register Trace provider by GUID + TraceLoggingRegister(g_ConhostLauncherProvider); + + // Pass command line and standard handles at this point in time as + // potential preferences for execution that were passed on process creation. + ConsoleArguments args(GetCommandLineW(), + GetStdHandle(STD_INPUT_HANDLE), + GetStdHandle(STD_OUTPUT_HANDLE)); + + HRESULT hr = args.ParseCommandline(); + if (SUCCEEDED(hr)) + { + // Only try to register as a handoff target if we are NOT a part of Windows. +#ifndef __INSIDE_WINDOWS + bool defAppEnabled = false; + if (args.ShouldRunAsComServer() && SUCCEEDED(Microsoft::Console::Internal::DefaultApp::CheckDefaultAppPolicy(defAppEnabled)) && defAppEnabled) + { + try + { + // OK we have to do this here and not in another method because + // we would either have to store the module ref above in some accessible + // variable (which would be awful because of the gigantic template name) + // or we would have to come up with some creativity to extract it out + // of the singleton module base without accidentally having WRL + // think we're recreating it (and then assert because it's already created.) + // + // Also this is all a problem because the decrementing count of used objects + // in this module in WRL::Module base doesn't null check the release notifier + // callback function in the OutOfProc variant in the 18362 SDK. So if anything + // else uses WRL directly or indirectly, it'll crash if the refcount + // ever hits 0. + // It does in the 19041 SDK so this can be cleaned into its own class if + // we ever build with 19041 or later. + auto comScope{ wil::CoInitializeEx(COINIT_MULTITHREADED) }; + + RETURN_IF_FAILED(module.RegisterObjects()); + _comServerExitEvent.wait(); + RETURN_IF_FAILED(module.UnregisterObjects()); + } + CATCH_RETURN() + } + else +#endif + { + if (ShouldUseLegacyConhost(args)) + { + if (args.ShouldCreateServerHandle()) + { + hr = E_INVALIDARG; + } + else + { + hr = ValidateServerHandle(args.GetServerHandle()); + + if (SUCCEEDED(hr)) + { + hr = ActivateLegacyConhost(args.GetServerHandle()); + } + } + } + else + { + if (args.ShouldCreateServerHandle()) + { + hr = Entrypoints::StartConsoleForCmdLine(args.GetClientCommandline().c_str(), &args); + } + else + { + hr = ValidateServerHandle(args.GetServerHandle()); + + if (SUCCEEDED(hr)) + { + hr = Entrypoints::StartConsoleForServerHandle(args.GetServerHandle(), &args); + } + } + } + } + } + + // Unregister Tracelogging + TraceLoggingUnregister(g_ConhostLauncherProvider); + + // Only do this if startup was successful. Otherwise, this will leave conhost.exe running with no hosted application. + if (SUCCEEDED(hr)) + { + // Since the lifetime of conhost.exe is inextricably tied to the lifetime of its client processes we set our process + // shutdown priority to zero in order to effectively opt out of shutdown process enumeration. Conhost will exit when + // all of its client processes do. + SetProcessShutdownParameters(0, 0); + + ExitThread(hr); + } + + return hr; +} diff --git a/src/host/exe/sources b/src/host/exe/sources index a82786447..1071fdb5c 100644 --- a/src/host/exe/sources +++ b/src/host/exe/sources @@ -32,7 +32,8 @@ CETCOMPAT=1 SOURCES = \ $(SOURCES) \ - ..\exemain.cpp \ + .\CConsoleHandoff.cpp \ + .\exemain.cpp \ ..\res.rc \ # ------------------------------------- diff --git a/src/host/exemain.cpp b/src/host/exemain.cpp deleted file mode 100644 index 8d338ff48..000000000 --- a/src/host/exemain.cpp +++ /dev/null @@ -1,227 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT license. - -#include "precomp.h" - -#include "ConsoleArguments.hpp" -#include "srvinit.h" -#include "../server/Entrypoints.h" -#include "../interactivity/inc/ServiceLocator.hpp" - -// Define TraceLogging provider -TRACELOGGING_DEFINE_PROVIDER( - g_ConhostLauncherProvider, - "Microsoft.Windows.Console.Launcher", - // {770aa552-671a-5e97-579b-151709ec0dbd} - (0x770aa552, 0x671a, 0x5e97, 0x57, 0x9b, 0x15, 0x17, 0x09, 0xec, 0x0d, 0xbd), - TraceLoggingOptionMicrosoftTelemetry()); - -static bool ConhostV2ForcedInRegistry() -{ - // If the registry value doesn't exist, or exists and is non-zero, we should default to using the v2 console. - // Otherwise, in the case of an explicit value of 0, we should use the legacy console. - bool fShouldUseConhostV2 = true; - PCSTR pszErrorDescription = nullptr; - bool fIgnoreError = false; - - // open HKCU\Console - wil::unique_hkey hConsoleSubKey; - LONG lStatus = NTSTATUS_FROM_WIN32(RegOpenKeyExW(HKEY_CURRENT_USER, L"Console", 0, KEY_READ, &hConsoleSubKey)); - if (ERROR_SUCCESS == lStatus) - { - // now get the value of the ForceV2 reg value, if it exists - DWORD dwValue; - DWORD dwType; - DWORD cbValue = sizeof(dwValue); - lStatus = RegQueryValueExW(hConsoleSubKey.get(), - L"ForceV2", - nullptr, - &dwType, - (PBYTE)&dwValue, - &cbValue); - - if (ERROR_SUCCESS == lStatus && - dwType == REG_DWORD && // response is a DWORD - cbValue == sizeof(dwValue)) // response data exists - { - // Value exists. If non-zero use v2 console. - fShouldUseConhostV2 = dwValue != 0; - } - else - { - pszErrorDescription = "RegQueryValueKey Failed"; - fIgnoreError = lStatus == ERROR_FILE_NOT_FOUND; - } - } - else - { - pszErrorDescription = "RegOpenKey Failed"; - // ignore error caused by RegOpenKey if it's a simple case of the key not being found - fIgnoreError = lStatus == ERROR_FILE_NOT_FOUND; - } - - return fShouldUseConhostV2; -} - -[[nodiscard]] static HRESULT ValidateServerHandle(const HANDLE handle) -{ - // Make sure this is a console file. - FILE_FS_DEVICE_INFORMATION DeviceInformation; - IO_STATUS_BLOCK IoStatusBlock; - NTSTATUS const Status = NtQueryVolumeInformationFile(handle, &IoStatusBlock, &DeviceInformation, sizeof(DeviceInformation), FileFsDeviceInformation); - if (!NT_SUCCESS(Status)) - { - RETURN_NTSTATUS(Status); - } - else if (DeviceInformation.DeviceType != FILE_DEVICE_CONSOLE) - { - return E_INVALIDARG; - } - else - { - return S_OK; - } -} - -static bool ShouldUseLegacyConhost(const ConsoleArguments& args) -{ - if (args.InConptyMode()) - { - return false; - } - - if (args.GetForceV1()) - { - return true; - } - - // Per the documentation in ConhostV2ForcedInRegistry, it checks the value - // of HKCU\Console:ForceV2. If it's *not found* or nonzero, "v2" is forced. - return !ConhostV2ForcedInRegistry(); -} - -[[nodiscard]] static HRESULT ActivateLegacyConhost(const HANDLE handle) -{ - HRESULT hr = S_OK; - - // TraceLog that we're using the legacy console. We won't log new console - // because there's already a count of how many total processes were launched. - // Total - legacy = new console. - // We expect legacy launches to be infrequent enough to not cause an issue. - TraceLoggingWrite(g_ConhostLauncherProvider, "IsLegacyLoaded", TraceLoggingBool(true, "ConsoleLegacy"), TraceLoggingKeyword(MICROSOFT_KEYWORD_TELEMETRY)); - - const PCWSTR pszConhostDllName = L"ConhostV1.dll"; - - // Load our implementation, and then Load/Launch the IO thread. - wil::unique_hmodule hConhostBin(LoadLibraryExW(pszConhostDllName, nullptr, LOAD_LIBRARY_SEARCH_SYSTEM32)); - if (hConhostBin.get() != nullptr) - { - typedef NTSTATUS (*PFNCONSOLECREATEIOTHREAD)(__in HANDLE Server); - - PFNCONSOLECREATEIOTHREAD pfnConsoleCreateIoThread = (PFNCONSOLECREATEIOTHREAD)GetProcAddress(hConhostBin.get(), "ConsoleCreateIoThread"); - if (pfnConsoleCreateIoThread != nullptr) - { - hr = HRESULT_FROM_NT(pfnConsoleCreateIoThread(handle)); - } - else - { - hr = HRESULT_FROM_WIN32(GetLastError()); - } - } - else - { - // setup status error - hr = HRESULT_FROM_WIN32(GetLastError()); - } - - if (SUCCEEDED(hr)) - { - hConhostBin.release(); - } - - return hr; -} - -// Routine Description: -// - Main entry point for EXE version of console launching. -// This can be used as a debugging/diagnostics tool as well as a method of testing the console without -// replacing the system binary. -// Arguments: -// - hInstance - This module instance pointer is saved for resource lookups. -// - hPrevInstance - Unused pointer to the module instances. See wWinMain definitions @ MSDN for more details. -// - pwszCmdLine - Unused variable. We will look up the command line using GetCommandLineW(). -// - nCmdShow - Unused variable specifying window show/hide state for Win32 mode applications. -// Return value: -// - [[noreturn]] - This function will not return. It will kill the thread we were called from and the console server threads will take over. -int CALLBACK wWinMain( - _In_ HINSTANCE hInstance, - _In_ HINSTANCE /*hPrevInstance*/, - _In_ PWSTR /*pwszCmdLine*/, - _In_ int /*nCmdShow*/) -{ - Microsoft::Console::Interactivity::ServiceLocator::LocateGlobals().hInstance = hInstance; - - ConsoleCheckDebug(); - - // Register Trace provider by GUID - TraceLoggingRegister(g_ConhostLauncherProvider); - - // Pass command line and standard handles at this point in time as - // potential preferences for execution that were passed on process creation. - ConsoleArguments args(GetCommandLineW(), - GetStdHandle(STD_INPUT_HANDLE), - GetStdHandle(STD_OUTPUT_HANDLE)); - - HRESULT hr = args.ParseCommandline(); - if (SUCCEEDED(hr)) - { - if (ShouldUseLegacyConhost(args)) - { - if (args.ShouldCreateServerHandle()) - { - hr = E_INVALIDARG; - } - else - { - hr = ValidateServerHandle(args.GetServerHandle()); - - if (SUCCEEDED(hr)) - { - hr = ActivateLegacyConhost(args.GetServerHandle()); - } - } - } - else - { - if (args.ShouldCreateServerHandle()) - { - hr = Entrypoints::StartConsoleForCmdLine(args.GetClientCommandline().c_str(), &args); - } - else - { - hr = ValidateServerHandle(args.GetServerHandle()); - - if (SUCCEEDED(hr)) - { - hr = Entrypoints::StartConsoleForServerHandle(args.GetServerHandle(), &args); - } - } - } - } - - // Unregister Tracelogging - TraceLoggingUnregister(g_ConhostLauncherProvider); - - // Only do this if startup was successful. Otherwise, this will leave conhost.exe running with no hosted application. - if (SUCCEEDED(hr)) - { - // Since the lifetime of conhost.exe is inextricably tied to the lifetime of its client processes we set our process - // shutdown priority to zero in order to effectively opt out of shutdown process enumeration. Conhost will exit when - // all of its client processes do. - SetProcessShutdownParameters(0, 0); - - ExitThread(hr); - } - - return hr; -} diff --git a/src/host/globals.h b/src/host/globals.h index 2bcfd2859..4edf2bbc4 100644 --- a/src/host/globals.h +++ b/src/host/globals.h @@ -73,6 +73,7 @@ public: bool handoffTarget = false; std::optional handoffConsoleClsid; + std::optional handoffTerminalClsid; #ifdef UNIT_TESTING void EnableConptyModeForTests(std::unique_ptr vtRenderEngine); diff --git a/src/host/host-common.vcxitems b/src/host/host-common.vcxitems index 0361b6f82..190eef792 100644 --- a/src/host/host-common.vcxitems +++ b/src/host/host-common.vcxitems @@ -121,7 +121,7 @@ - %(AdditionalIncludeDirectories) + $(IntDir)..\OpenConsoleProxy;%(AdditionalIncludeDirectories) diff --git a/src/host/lib/hostlib.vcxproj b/src/host/lib/hostlib.vcxproj index adc112419..fb83977af 100644 --- a/src/host/lib/hostlib.vcxproj +++ b/src/host/lib/hostlib.vcxproj @@ -6,7 +6,7 @@ hostlib Host ConhostV2Lib - StaticLibrary + StaticLibrary diff --git a/src/host/lib/hostlib.vcxproj.filters b/src/host/lib/hostlib.vcxproj.filters index bd95d7351..e8877f96c 100644 --- a/src/host/lib/hostlib.vcxproj.filters +++ b/src/host/lib/hostlib.vcxproj.filters @@ -30,9 +30,6 @@ Source Files - - Source Files - Source Files @@ -162,9 +159,6 @@ Source Files - - Source Files - Source Files @@ -183,6 +177,12 @@ Source Files + + Source Files + + + Source Files + @@ -254,9 +254,6 @@ Header Files - - Header Files - Header Files @@ -332,9 +329,6 @@ Header Files - - Header Files - Header Files @@ -353,5 +347,17 @@ Header Files + + Header Files + + + Header Files + + + Header Files + - + + + + \ No newline at end of file diff --git a/src/host/proxy/Host.Proxy.vcxproj b/src/host/proxy/Host.Proxy.vcxproj index 412466115..9d479599e 100644 --- a/src/host/proxy/Host.Proxy.vcxproj +++ b/src/host/proxy/Host.Proxy.vcxproj @@ -1,17 +1,14 @@ - {E437B604-3E98-4F40-A927-E173E818EA4B} + {71CC9D78-BA29-4D93-946F-BEF5D9A3A6EF} Win32Proj openconsoleproxy OpenConsoleProxy OpenConsoleProxy - Utility + DynamicLibrary - - - + REGISTER_PROXY_DLL;%(PreprocessorDefinitions) NotUsing + + OpenConsoleProxy.def + diff --git a/src/host/proxy/Host.Proxy.vcxproj.filters b/src/host/proxy/Host.Proxy.vcxproj.filters index 103c008eb..df6bcbd10 100644 --- a/src/host/proxy/Host.Proxy.vcxproj.filters +++ b/src/host/proxy/Host.Proxy.vcxproj.filters @@ -14,11 +14,31 @@ rc;ico;cur;bmp;dlg;rc2;rct;bin;rgs;gif;jpg;jpeg;jpe;resx;tiff;tif;png;wav;mfcribbon-ms + + + Source Files + + + Source Files + + + Source Files + + + Source Files + + + Source Files + + - + + Header Files + + Header Files @@ -26,5 +46,8 @@ Source Files + + Source Files + \ No newline at end of file diff --git a/src/host/proxy/IConsoleHandoff.idl b/src/host/proxy/IConsoleHandoff.idl index f96469852..067eb0efa 100644 --- a/src/host/proxy/IConsoleHandoff.idl +++ b/src/host/proxy/IConsoleHandoff.idl @@ -1,3 +1,6 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + import "oaidl.idl"; import "ocidl.idl"; @@ -24,3 +27,4 @@ typedef const CONSOLE_PORTABLE_ATTACH_MSG* PCCONSOLE_PORTABLE_ATTACH_MSG; [in, system_handle(sh_event)] HANDLE inputEvent, [in, ref] PCCONSOLE_PORTABLE_ATTACH_MSG msg); }; + diff --git a/src/host/proxy/ITerminalHandoff.idl b/src/host/proxy/ITerminalHandoff.idl new file mode 100644 index 000000000..79c356908 --- /dev/null +++ b/src/host/proxy/ITerminalHandoff.idl @@ -0,0 +1,16 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +import "oaidl.idl"; +import "ocidl.idl"; + +[ + object, + uuid(FA1E3AB4-9AEC-4A3C-96CA-E6078C30BD74) +] interface ITerminalHandoff : IUnknown +{ + HRESULT EstablishPtyHandoff([in, system_handle(sh_pipe)] HANDLE in, + [in, system_handle(sh_pipe)] HANDLE out, + [in, system_handle(sh_pipe)] HANDLE signal, + [in, system_handle(sh_process)] HANDLE client); +}; diff --git a/src/host/proxy/OpenConsoleProxy.def b/src/host/proxy/OpenConsoleProxy.def new file mode 100644 index 000000000..e41123ab1 --- /dev/null +++ b/src/host/proxy/OpenConsoleProxy.def @@ -0,0 +1,8 @@ +LIBRARY OpenConsoleProxy + +EXPORTS + DllCanUnloadNow PRIVATE + DllGetClassObject PRIVATE + DllRegisterServer PRIVATE + DllUnregisterServer PRIVATE + GetProxyDllInfo diff --git a/src/host/srvinit.cpp b/src/host/srvinit.cpp index 88a7d85c9..ad43d06ed 100644 --- a/src/host/srvinit.cpp +++ b/src/host/srvinit.cpp @@ -23,6 +23,7 @@ #include "renderData.hpp" #include "../renderer/base/renderer.hpp" +#include "ITerminalHandoff.h" #include "../inc/conint.h" #include "../propslib/DelegationConfig.hpp" @@ -60,6 +61,10 @@ try { Globals.handoffConsoleClsid = delegationClsid; } + if (SUCCEEDED(DelegationConfig::s_GetDefaultTerminalId(delegationClsid))) + { + Globals.handoffTerminalClsid = delegationClsid; + } } // Removed allocation of scroll buffer here. @@ -265,20 +270,53 @@ void ConsoleCheckDebug() #endif } -[[nodiscard]] HRESULT ConsoleCreateIoThreadLegacy(_In_ HANDLE Server, const ConsoleArguments* const args) +// Routine Description: +// - Sets up the main driver message packet (I/O) processing +// thread that will handle all client requests from all +// attached command-line applications for the duration +// of this console server session. +// - The optional arguments are only used when receiving a handoff +// from another console server (typically in-box to the Windows OS image) +// that has already started processing the console session. +// They will be blank and generated internally by this method if this is the first +// console server starting in response to a client startup or ConPTY setup +// request. +// Arguments: +// - Server - Handle to the console driver that represents +// our server side of the connection. +// - args - Command-line arguments from starting this console host +// that may affect the way we host the session. +// - driverInputEvent - (Optional) Event registered with the console driver +// that we will use to wake up input read requests that +// are blocked because they came in when we had no input ready. +// - connectMessage - (Optional) A message received from a connecting client +// by another console server that is being passed off to us as a part of +// the handoff strategy. +HRESULT ConsoleCreateIoThread(_In_ HANDLE Server, + const ConsoleArguments* const args, + HANDLE driverInputEvent, + PCONSOLE_API_MSG connectMessage) { auto& g = ServiceLocator::LocateGlobals(); RETURN_IF_FAILED(ConsoleServerInitialization(Server, args)); RETURN_IF_FAILED(g.hConsoleInputInitEvent.create(wil::EventOptions::None)); - // Set up and tell the driver about the input available event. - RETURN_IF_FAILED(g.hInputEvent.create(wil::EventOptions::ManualReset)); + if (driverInputEvent != INVALID_HANDLE_VALUE) + { + // Store the driver input event. It's already been told that it exists by whomever started us. + g.hInputEvent.reset(driverInputEvent); + } + else + { + // Set up and tell the driver about the input available event. + RETURN_IF_FAILED(g.hInputEvent.create(wil::EventOptions::ManualReset)); - CD_IO_SERVER_INFORMATION ServerInformation; - ServerInformation.InputAvailableEvent = ServiceLocator::LocateGlobals().hInputEvent.get(); - RETURN_IF_FAILED(g.pDeviceComm->SetServerInformation(&ServerInformation)); + CD_IO_SERVER_INFORMATION ServerInformation; + ServerInformation.InputAvailableEvent = ServiceLocator::LocateGlobals().hInputEvent.get(); + RETURN_IF_FAILED(g.pDeviceComm->SetServerInformation(&ServerInformation)); + } - HANDLE const hThread = CreateThread(nullptr, 0, ConsoleIoThread, nullptr, 0, nullptr); + HANDLE const hThread = CreateThread(nullptr, 0, ConsoleIoThread, connectMessage, 0, nullptr); RETURN_HR_IF(E_HANDLE, hThread == nullptr); LOG_IF_WIN32_BOOL_FALSE(CloseHandle(hThread)); // The thread will run on its own and close itself. Free the associated handle. @@ -295,6 +333,117 @@ void ConsoleCheckDebug() return S_OK; } +// Routine Description: +// - Accepts a console server session from another console server +// most commonly from the operating system in-box console to +// a more-up-to-date and out-of-band delivered one. +// Arguments: +// - Server - Handle to the console driver that represents our server +// side of hosting the console session +// - driverInputEvent - Handle to an event already registered with the +// driver that clients will implicitly wait on when we don't have +// any input to return in the queue when a request is made and is +// signaled to unblock them when input finally arrives. +// - connectMessage - A console driver/server message as received +// by the previous console server for us to finish processing in +// order to complete the client's initial connection and store +// all necessary callback information for all subsequent API calls. +// Return Value: +// - COM errors, registry errors, pipe errors, handle manipulation errors, +// errors from the creating the thread for the +// standard IO thread loop for the server to process messages +// from the driver... or an S_OK success. +[[nodiscard]] HRESULT ConsoleEstablishHandoff(_In_ HANDLE Server, + HANDLE driverInputEvent, + PCONSOLE_API_MSG connectMessage) +try +{ + auto& g = ServiceLocator::LocateGlobals(); + g.handoffTarget = true; + + IID delegationClsid; + if (SUCCEEDED(DelegationConfig::s_GetDefaultConsoleId(delegationClsid))) + { + g.handoffConsoleClsid = delegationClsid; + } + if (SUCCEEDED(DelegationConfig::s_GetDefaultTerminalId(delegationClsid))) + { + g.handoffTerminalClsid = delegationClsid; + } + + if (!g.handoffTerminalClsid) + { + return E_NOT_SET; + } + + wil::unique_handle signalPipeTheirSide; + wil::unique_handle signalPipeOurSide; + + wil::unique_handle inPipeTheirSide; + wil::unique_handle inPipeOurSide; + + wil::unique_handle outPipeTheirSide; + wil::unique_handle outPipeOurSide; + + SECURITY_ATTRIBUTES sa; + sa.nLength = sizeof(sa); + // Mark inheritable for signal handle when creating. It'll have the same value on the other side. + sa.bInheritHandle = TRUE; + sa.lpSecurityDescriptor = nullptr; + + RETURN_IF_WIN32_BOOL_FALSE(CreatePipe(signalPipeOurSide.addressof(), signalPipeTheirSide.addressof(), nullptr, 0)); + RETURN_IF_WIN32_BOOL_FALSE(SetHandleInformation(signalPipeTheirSide.get(), HANDLE_FLAG_INHERIT, HANDLE_FLAG_INHERIT)); + + RETURN_IF_WIN32_BOOL_FALSE(CreatePipe(inPipeOurSide.addressof(), inPipeTheirSide.addressof(), nullptr, 0)); + RETURN_IF_WIN32_BOOL_FALSE(SetHandleInformation(inPipeTheirSide.get(), HANDLE_FLAG_INHERIT, HANDLE_FLAG_INHERIT)); + + RETURN_IF_WIN32_BOOL_FALSE(CreatePipe(outPipeTheirSide.addressof(), outPipeOurSide.addressof(), nullptr, 0)); + RETURN_IF_WIN32_BOOL_FALSE(SetHandleInformation(outPipeTheirSide.get(), HANDLE_FLAG_INHERIT, HANDLE_FLAG_INHERIT)); + + wil::unique_handle clientProcess{ OpenProcess(PROCESS_QUERY_INFORMATION | SYNCHRONIZE, TRUE, static_cast(connectMessage->Descriptor.Process)) }; + RETURN_LAST_ERROR_IF_NULL(clientProcess.get()); + + ::Microsoft::WRL::ComPtr handoff; + + RETURN_IF_FAILED(CoCreateInstance(g.handoffTerminalClsid.value(), nullptr, CLSCTX_LOCAL_SERVER, IID_PPV_ARGS(&handoff))); + + RETURN_IF_FAILED(handoff->EstablishPtyHandoff(inPipeTheirSide.get(), + outPipeTheirSide.get(), + signalPipeTheirSide.get(), + clientProcess.get())); + + inPipeTheirSide.release(); + outPipeTheirSide.release(); + signalPipeTheirSide.release(); + + const auto commandLine = fmt::format(FMT_COMPILE(L" --headless --signal {:#x}"), (int64_t)signalPipeOurSide.release()); + + ConsoleArguments consoleArgs(commandLine, inPipeOurSide.release(), outPipeOurSide.release()); + RETURN_IF_FAILED(consoleArgs.ParseCommandline()); + + return ConsoleCreateIoThread(Server, &consoleArgs, driverInputEvent, connectMessage); +} +CATCH_RETURN() + +// Routine Description: +// - Creates the I/O thread for handling and processing messages from the console driver +// as the server side of a console session. +// - This entrypoint is for all start scenarios that are not receiving a hand-off +// from another console server. For example, getting started by kernelbase.dll from +// the operating system as a client application realizes it needs a console server, +// getting started to be a ConPTY host inside the OS, or being double clicked either +// inside the OS as `conhost.exe` or outside as `OpenConsole.exe`. +// Arguments: +// - Server - The server side handle to the console driver to let us pick up messages to process for the clients. +// - args - A structure of arguments that may have been passed in on the command-line, typically only used to control the ConPTY configuration. +// Return Value: +// - S_OK if the thread starts up correctly or any number of thread, registry, windowing, or just about any other +// failure that could possibly occur during console server initialization. +[[nodiscard]] HRESULT ConsoleCreateIoThreadLegacy(_In_ HANDLE Server, const ConsoleArguments* const args) +{ + return ConsoleCreateIoThread(Server, args, INVALID_HANDLE_VALUE, nullptr); +} + #define SYSTEM_ROOT (L"%SystemRoot%") #define SYSTEM_ROOT_LENGTH (sizeof(SYSTEM_ROOT) - sizeof(WCHAR)) @@ -662,10 +811,10 @@ PWSTR TranslateConsoleTitle(_In_ PCWSTR pwszConsoleTitle, const BOOL fUnexpand, // - This routine is the main one in the console server IO thread. // - It reads IO requests submitted by clients through the driver, services and completes them in a loop. // Arguments: -// - +// - lpParameter - PCONSOLE_API_MSG being handed off to us from the previous I/O. // Return Value: // - This routine never returns. The process exits when no more references or clients exist. -DWORD WINAPI ConsoleIoThread(LPVOID /*lpParameter*/) +DWORD WINAPI ConsoleIoThread(LPVOID lpParameter) { auto& globals = ServiceLocator::LocateGlobals(); @@ -674,6 +823,15 @@ DWORD WINAPI ConsoleIoThread(LPVOID /*lpParameter*/) ReceiveMsg._pDeviceComm = globals.pDeviceComm; PCONSOLE_API_MSG ReplyMsg = nullptr; + // If we were given a message on startup, process that in our context and then continue with the IO loop normally. + if (lpParameter) + { + ReceiveMsg = *(PCONSOLE_API_MSG)lpParameter; + ReceiveMsg._pApiRoutines = &globals.api; + ReceiveMsg._pDeviceComm = globals.pDeviceComm; + IoSorter::ServiceIoOperation(&ReceiveMsg, &ReplyMsg); + } + bool fShouldExit = false; while (!fShouldExit) { diff --git a/src/host/srvinit.h b/src/host/srvinit.h index 58fbb3035..3b2725a39 100644 --- a/src/host/srvinit.h +++ b/src/host/srvinit.h @@ -28,4 +28,8 @@ PWSTR TranslateConsoleTitle(_In_ PCWSTR pwszConsoleTitle, const BOOL fUnexpand, [[nodiscard]] bool ConsoleConnectionDeservesVisibleWindow(PCONSOLE_API_CONNECTINFO p); +[[nodiscard]] HRESULT ConsoleEstablishHandoff(_In_ HANDLE Server, + HANDLE driverInputEvent, + PCONSOLE_API_MSG connectMessage); + void ConsoleCheckDebug(); diff --git a/src/host/ut_host/ConsoleArgumentsTests.cpp b/src/host/ut_host/ConsoleArgumentsTests.cpp index 7a23a5995..b210899b7 100644 --- a/src/host/ut_host/ConsoleArgumentsTests.cpp +++ b/src/host/ut_host/ConsoleArgumentsTests.cpp @@ -81,7 +81,8 @@ void ConsoleArgumentsTests::ArgSplittingTests() true, // createServerHandle 0, // serverHandle 0, // signalHandle - false), // inheritCursor + false, // inheritCursor + false), // runAsComServer true); // successful parse? commandline = L"conhost.exe \"this is the commandline\""; @@ -101,7 +102,8 @@ void ConsoleArgumentsTests::ArgSplittingTests() true, // createServerHandle 0, // serverHandle 0, // signalHandle - false), // inheritCursor + false, // inheritCursor + false), // runAsComServer true); // successful parse? commandline = L"conhost.exe --headless \"--vtmode bar this is the commandline\""; @@ -121,7 +123,8 @@ void ConsoleArgumentsTests::ArgSplittingTests() true, // createServerHandle 0, // serverHandle 0, // signalHandle - false), // inheritCursor + false, // inheritCursor + false), // runAsComServer true); // successful parse? commandline = L"conhost.exe --headless --server 0x4 this is the commandline"; @@ -141,7 +144,8 @@ void ConsoleArgumentsTests::ArgSplittingTests() false, // createServerHandle 0x4, // serverHandle 0, // signalHandle - false), // inheritCursor + false, // inheritCursor + false), // runAsComServer true); // successful parse? commandline = L"conhost.exe --headless\t--vtmode\txterm\tthis\tis\tthe\tcommandline"; @@ -161,7 +165,8 @@ void ConsoleArgumentsTests::ArgSplittingTests() true, // createServerHandle 0, // serverHandle 0, // signalHandle - false), // inheritCursor + false, // inheritCursor + false), // runAsComServer true); // successful parse? commandline = L"conhost.exe --headless\\ foo\\ --outpipe\\ bar\\ this\\ is\\ the\\ commandline"; @@ -181,7 +186,8 @@ void ConsoleArgumentsTests::ArgSplittingTests() true, // createServerHandle 0, // serverHandle 0, // signalHandle - false), // inheritCursor + false, // inheritCursor + false), // runAsComServer true); // successful parse? commandline = L"conhost.exe --headless\\\tfoo\\\t--outpipe\\\tbar\\\tthis\\\tis\\\tthe\\\tcommandline"; @@ -201,7 +207,8 @@ void ConsoleArgumentsTests::ArgSplittingTests() true, // createServerHandle 0, // serverHandle 0, // signalHandle - false), // inheritCursor + false, // inheritCursor + false), // runAsComServer true); // successful parse? commandline = L"conhost.exe --vtmode a\\\\\\\\\"b c\" d e"; @@ -221,7 +228,8 @@ void ConsoleArgumentsTests::ArgSplittingTests() true, // createServerHandle 0, // serverHandle 0, // signalHandle - false), // inheritCursor + false, // inheritCursor + false), // runAsComServer true); // successful parse? commandline = L"conhost.exe this is the commandline"; @@ -241,7 +249,8 @@ void ConsoleArgumentsTests::ArgSplittingTests() true, // createServerHandle 0, // serverHandle 0, // signalHandle - false), // inheritCursor + false, // inheritCursor + false), // runAsComServer true); // successful parse? } @@ -266,7 +275,8 @@ void ConsoleArgumentsTests::ClientCommandlineTests() true, // createServerHandle 0, // serverHandle 0, // signalHandle - false), // inheritCursor + false, // inheritCursor + false), // runAsComServer true); // successful parse? commandline = L"conhost.exe foo"; @@ -286,7 +296,8 @@ void ConsoleArgumentsTests::ClientCommandlineTests() true, // createServerHandle 0, // serverHandle 0, // signalHandle - false), // inheritCursor + false, // inheritCursor + false), // runAsComServer true); // successful parse? commandline = L"conhost.exe foo -- bar"; @@ -306,7 +317,8 @@ void ConsoleArgumentsTests::ClientCommandlineTests() true, // createServerHandle 0, // serverHandle 0, // signalHandle - false), // inheritCursor + false, // inheritCursor + false), // runAsComServer true); // successful parse? commandline = L"conhost.exe --vtmode foo foo -- bar"; @@ -326,7 +338,8 @@ void ConsoleArgumentsTests::ClientCommandlineTests() true, // createServerHandle 0, // serverHandle 0, // signalHandle - false), // inheritCursor + false, // inheritCursor + false), // runAsComServer true); // successful parse? commandline = L"conhost.exe console --vtmode foo foo -- bar"; @@ -346,7 +359,8 @@ void ConsoleArgumentsTests::ClientCommandlineTests() true, // createServerHandle 0, // serverHandle 0, // signalHandle - false), // inheritCursor + false, // inheritCursor + false), // runAsComServer true); // successful parse? commandline = L"conhost.exe console --vtmode foo --outpipe foo -- bar"; @@ -366,7 +380,8 @@ void ConsoleArgumentsTests::ClientCommandlineTests() true, // createServerHandle 0, // serverHandle 0, // signalHandle - false), // inheritCursor + false, // inheritCursor + false), // runAsComServer true); // successful parse? commandline = L"conhost.exe --vtmode foo -- --outpipe foo bar"; @@ -386,7 +401,8 @@ void ConsoleArgumentsTests::ClientCommandlineTests() true, // createServerHandle 0, // serverHandle 0, // signalHandle - false), // inheritCursor + false, // inheritCursor + false), // runAsComServer true); // successful parse? commandline = L"conhost.exe --vtmode -- --headless bar"; @@ -406,7 +422,8 @@ void ConsoleArgumentsTests::ClientCommandlineTests() true, // createServerHandle 0, // serverHandle 0, // signalHandle - false), // inheritCursor + false, // inheritCursor + false), // runAsComServer true); // successful parse? commandline = L"conhost.exe --"; @@ -426,7 +443,8 @@ void ConsoleArgumentsTests::ClientCommandlineTests() true, // createServerHandle 0, // serverHandle 0, // signalHandle - false), // inheritCursor + false, // inheritCursor + false), // runAsComServer true); // successful parse? commandline = L"conhost.exe"; @@ -446,7 +464,8 @@ void ConsoleArgumentsTests::ClientCommandlineTests() true, // createServerHandle 0, // serverHandle 0, // signalHandle - false), // inheritCursor + false, // inheritCursor + false), // runAsComServer true); // successful parse? } @@ -471,7 +490,8 @@ void ConsoleArgumentsTests::LegacyFormatsTests() false, // createServerHandle 4ul, // serverHandle 0, // signalHandle - false), // inheritCursor + false, // inheritCursor + false), // runAsComServer true); // successful parse? commandline = L"conhost.exe --server 0x4"; @@ -491,7 +511,8 @@ void ConsoleArgumentsTests::LegacyFormatsTests() false, // createServerHandle 4ul, // serverHandle 0, // signalHandle - false), // inheritCursor + false, // inheritCursor + false), // runAsComServer true); // successful parse? commandline = L"conhost.exe 0x4 0x8"; @@ -511,7 +532,8 @@ void ConsoleArgumentsTests::LegacyFormatsTests() false, // createServerHandle 4ul, // serverHandle 0, // signalHandle - false), // inheritCursor + false, // inheritCursor + false), // runAsComServer false); // successful parse? commandline = L"conhost.exe --server 0x4 0x8"; @@ -531,7 +553,8 @@ void ConsoleArgumentsTests::LegacyFormatsTests() false, // createServerHandle 4ul, // serverHandle 0, // signalHandle - false), // inheritCursor + false, // inheritCursor + false), // runAsComServer false); // successful parse? commandline = L"conhost.exe 0x4 --server 0x8"; @@ -551,7 +574,8 @@ void ConsoleArgumentsTests::LegacyFormatsTests() false, // createServerHandle 4ul, // serverHandle 0, // signalHandle - false), // inheritCursor + false, // inheritCursor + false), // runAsComServer false); // successful parse? commandline = L"conhost.exe --server 0x4 --server 0x8"; @@ -571,7 +595,8 @@ void ConsoleArgumentsTests::LegacyFormatsTests() false, // createServerHandle 4ul, // serverHandle 0, // signalHandle - false), // inheritCursor + false, // inheritCursor + false), // runAsComServer false); // successful parse? commandline = L"conhost.exe 0x4 -ForceV1"; @@ -591,7 +616,8 @@ void ConsoleArgumentsTests::LegacyFormatsTests() false, // createServerHandle 4ul, // serverHandle 0, // signalHandle - false), // inheritCursor + false, // inheritCursor + false), // runAsComServer true); // successful parse? commandline = L"conhost.exe -ForceV1"; @@ -611,7 +637,8 @@ void ConsoleArgumentsTests::LegacyFormatsTests() true, // createServerHandle 0, // serverHandle 0, // signalHandle - false), // inheritCursor + false, // inheritCursor + false), // runAsComServer true); // successful parse? } @@ -660,7 +687,8 @@ void ConsoleArgumentsTests::CombineVtPipeHandleTests() true, // createServerHandle 0ul, // serverHandle 0, // signalHandle - false), // inheritCursor + false, // inheritCursor + false), // runAsComServer true); // successful parse? commandline = L"conhost.exe --vtmode xterm-256color"; @@ -680,7 +708,8 @@ void ConsoleArgumentsTests::CombineVtPipeHandleTests() true, // createServerHandle 0ul, // serverHandle 0, // signalHandle - false), // inheritCursor + false, // inheritCursor + false), // runAsComServer true); // successful parse? } @@ -715,7 +744,8 @@ void ConsoleArgumentsTests::InitialSizeTests() true, // createServerHandle 0ul, // serverHandle 0, // signalHandle - false), // inheritCursor + false, // inheritCursor + false), // runAsComServer true); // successful parse? commandline = L"conhost.exe --width 120"; @@ -735,7 +765,8 @@ void ConsoleArgumentsTests::InitialSizeTests() true, // createServerHandle 0ul, // serverHandle 0, // signalHandle - false), // inheritCursor + false, // inheritCursor + false), // runAsComServer true); // successful parse? commandline = L"conhost.exe --height 30"; @@ -755,7 +786,8 @@ void ConsoleArgumentsTests::InitialSizeTests() true, // createServerHandle 0ul, // serverHandle 0, // signalHandle - false), // inheritCursor + false, // inheritCursor + false), // runAsComServer true); // successful parse? commandline = L"conhost.exe --width 0"; @@ -775,7 +807,8 @@ void ConsoleArgumentsTests::InitialSizeTests() true, // createServerHandle 0ul, // serverHandle 0, // signalHandle - false), // inheritCursor + false, // inheritCursor + false), // runAsComServer true); // successful parse? commandline = L"conhost.exe --width -1"; @@ -795,7 +828,8 @@ void ConsoleArgumentsTests::InitialSizeTests() true, // createServerHandle 0ul, // serverHandle 0, // signalHandle - false), // inheritCursor + false, // inheritCursor + false), // runAsComServer true); // successful parse? commandline = L"conhost.exe --width foo"; @@ -815,7 +849,8 @@ void ConsoleArgumentsTests::InitialSizeTests() true, // createServerHandle 0ul, // serverHandle 0, // signalHandle - false), // inheritCursor + false, // inheritCursor + false), // runAsComServer false); // successful parse? commandline = L"conhost.exe --width 2foo"; @@ -835,7 +870,8 @@ void ConsoleArgumentsTests::InitialSizeTests() true, // createServerHandle 0ul, // serverHandle 0, // signalHandle - false), // inheritCursor + false, // inheritCursor + false), // runAsComServer false); // successful parse? commandline = L"conhost.exe --width 65535"; @@ -855,7 +891,8 @@ void ConsoleArgumentsTests::InitialSizeTests() true, // createServerHandle 0ul, // serverHandle 0, // signalHandle - false), // inheritCursor + false, // inheritCursor + false), // runAsComServer false); // successful parse? } @@ -880,7 +917,8 @@ void ConsoleArgumentsTests::HeadlessArgTests() true, // createServerHandle 0, // serverHandle 0, // signalHandle - false), // inheritCursor + false, // inheritCursor + false), // runAsComServer true); // successful parse? commandline = L"conhost.exe --headless 0x4"; @@ -900,7 +938,8 @@ void ConsoleArgumentsTests::HeadlessArgTests() false, // createServerHandle 4ul, // serverHandle 0, // signalHandle - false), // inheritCursor + false, // inheritCursor + false), // runAsComServer true); // successful parse? commandline = L"conhost.exe --headless --headless"; @@ -920,7 +959,8 @@ void ConsoleArgumentsTests::HeadlessArgTests() true, // createServerHandle 0, // serverHandle 0, // signalHandle - false), // inheritCursor + false, // inheritCursor + false), // runAsComServer true); // successful parse? commandline = L"conhost.exe -- foo.exe --headless"; @@ -940,7 +980,8 @@ void ConsoleArgumentsTests::HeadlessArgTests() true, // createServerHandle 0, // serverHandle 0, // signalHandle - false), // inheritCursor + false, // inheritCursor + false), // runAsComServer true); // successful parse? } @@ -969,7 +1010,8 @@ void ConsoleArgumentsTests::SignalHandleTests() false, // createServerHandle 4ul, // serverHandle 8ul, // signalHandle - false), // inheritCursor + false, // inheritCursor + false), // runAsComServer true); // successful parse? commandline = L"conhost.exe --server 0x4 --signal ASDF"; @@ -989,7 +1031,8 @@ void ConsoleArgumentsTests::SignalHandleTests() false, // createServerHandle 4ul, // serverHandle 0ul, // signalHandle - false), // inheritCursor + false, // inheritCursor + false), // runAsComServer false); // successful parse? commandline = L"conhost.exe --signal --server 0x4"; @@ -1009,7 +1052,8 @@ void ConsoleArgumentsTests::SignalHandleTests() true, // createServerHandle 0ul, // serverHandle 0ul, // signalHandle - false), // inheritCursor + false, // inheritCursor + false), // runAsComServer false); // successful parse? } @@ -1038,7 +1082,8 @@ void ConsoleArgumentsTests::FeatureArgTests() true, // createServerHandle 0, // serverHandle 0, // signalHandle - false), // inheritCursor + false, // inheritCursor + false), // runAsComServer true); // successful parse? commandline = L"conhost.exe --feature tty"; ArgTestsRunner(L"#2 Error case, pass an unsupported feature", @@ -1057,7 +1102,8 @@ void ConsoleArgumentsTests::FeatureArgTests() true, // createServerHandle 0, // serverHandle 0, // signalHandle - false), // inheritCursor + false, // inheritCursor + false), // runAsComServer false); // successful parse? commandline = L"conhost.exe --feature pty --feature pty"; @@ -1077,7 +1123,8 @@ void ConsoleArgumentsTests::FeatureArgTests() true, // createServerHandle 0, // serverHandle 0, // signalHandle - false), // inheritCursor + false, // inheritCursor + false), // runAsComServer true); // successful parse? commandline = L"conhost.exe --feature pty --feature tty"; @@ -1097,7 +1144,8 @@ void ConsoleArgumentsTests::FeatureArgTests() true, // createServerHandle 0, // serverHandle 0, // signalHandle - false), // inheritCursor + false, // inheritCursor + false), // runAsComServer false); // successful parse? commandline = L"conhost.exe --feature pty --feature"; @@ -1117,7 +1165,8 @@ void ConsoleArgumentsTests::FeatureArgTests() true, // createServerHandle 0, // serverHandle 0, // signalHandle - false), // inheritCursor + false, // inheritCursor + false), // runAsComServer false); // successful parse? commandline = L"conhost.exe --feature pty --feature --signal foo"; @@ -1137,6 +1186,7 @@ void ConsoleArgumentsTests::FeatureArgTests() true, // createServerHandle 0, // serverHandle 0, // signalHandle - false), // inheritCursor + false, // inheritCursor + false), // runAsComServer false); // successful parse? } diff --git a/src/internal/stubs.cpp b/src/internal/stubs.cpp index ac312a23e..f9570d6dd 100644 --- a/src/internal/stubs.cpp +++ b/src/internal/stubs.cpp @@ -32,6 +32,8 @@ void EdpPolicy::AuditClipboard(const std::wstring_view /*destinationName*/) noex [[nodiscard]] HRESULT DefaultApp::CheckDefaultAppPolicy(bool& isEnabled) noexcept { - isEnabled = false; + // True so propsheet will show configuration options but be sure that + // the open one won't attempt handoff from double click of OpenConsole.exe + isEnabled = true; return S_OK; -} \ No newline at end of file +} diff --git a/src/server/IoDispatchers.cpp b/src/server/IoDispatchers.cpp index 03ca35798..58909c7d9 100644 --- a/src/server/IoDispatchers.cpp +++ b/src/server/IoDispatchers.cpp @@ -146,6 +146,19 @@ static bool _shouldAttemptHandoff(const Globals& globals, const CONSOLE_INFORMATION& gci, CONSOLE_API_CONNECTINFO& cac) { +#ifndef __INSIDE_WINDOWS + + UNREFERENCED_PARAMETER(globals); + UNREFERENCED_PARAMETER(gci); + UNREFERENCED_PARAMETER(cac); + + // If we are outside of Windows, do not attempt a handoff + // to another target as handoff is an inbox escape mechanism + // to get to this copy! + return false; + +#else + // This console is already initialized. Do not // attempt handoff to another one. // Note you can have a non-attach secondary connect for a child process @@ -209,6 +222,7 @@ static bool _shouldAttemptHandoff(const Globals& globals, } return true; +#endif } // Routine Description: