From bc97af701e4061a18da111dbd00f5a766c6dfb13 Mon Sep 17 00:00:00 2001 From: Mike Griese Date: Wed, 12 Jan 2022 05:56:43 -0600 Subject: [PATCH] Profile auto-elevation, version 3 (#12137) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Summary of the Pull Request This is the resurrection of #8514 and #11310. WE determined that we didn't want to do #11308 after all, so this should be profile auto-elevation, without the warning. This PR adds two features: * the `elevate: bool` property to profiles - If the user is running unelevated, **and** `elevate` is set to `true`, then instead of opening a new tab, we'll open an elevated Terminal window with the profile. - Otherwise, we'll just open a new tab in the existing window. This includes cases where the window is elevated, and the profile is set to `elevate:false`. `elevate:false` basically just means "do nothing special with me". * the `elevate: bool?` property to `NewTerminalArgs` (`newTab`, `splitPane`) - This allows a user to create an action that will elevate the profile, even if the profile is not otherwise set to auto-elevate. - `elevate:null` (_the default_) does not change the profile's elevation status. The action will use whatever is set by the profile. - `elevate:true` will attempt to auto-elevate the profile - `elevate:false` will do nothing special. ## References * #5000 for obvious reasons * Spec'd in #8455 ## PR Checklist * [x] Closes #632 * [x] I work here * [x] Tests added/passed * [ ] Requires documentation to be updated - sure does, but that'll come all at the end. ## Detailed Description of the Pull Request / Additional comments After playing with de-elevation a bit, it turns out it behaves weirdly with packaged applications. I can try and ask `explorer.exe` to launch the process on our behalf. However, if the thing we're launching is an execution alias (`wt.exe`), and we're elevated, then the child process will _still launch elevated_. There's also something super BODGEY at work here. `ShellExecute` is the function we use to ask the OS to elevate something for us. But `ShellExecute` needs to be able to send a window message to the process that called it (if the caller was a WINDOWS subsystem application). So if we die immediately after calling `ShellExecute`, then the elevated process never actually spawns - sad. So we're adding a helper process, `elevate-shim.exe`, that lives in our process. That'll be the one that actually calls `ShellExecute`, so that it can live for the duration of the UAC prompt. ## Validation Steps Performed * Ran tests * Opened a bunch of terminal tabs at various different elevation levels * opened new splits too * In the defaults (base layer) as well, for madness Some settings to use for testing
```jsonc "keybindings" : [ ////////// ELEVATION /////////////// { "keys": "f1", "name": "ELEVATED TAB", "icon": "\uEA18", "command": { "action": "newTab", "elevate": true } }, { "keys": "f2", "name": "ELEVATED, Color", "icon": "\uEA18", "command": { "action": "newTab", "elevate": true, "commandline": "PowerShell.exe", "startingDirectory": "C:\\Windows", "tabColor": "#bbaa00" } }, { "keys": "f3", "name": "unelevated ELEVATED", "icon": "🙃", "command": { "action": "newTab", "elevate": false, "profile": "elevated cmd" } }, ////////////////////////////// ], "profiles": { "defaults": { "elevate": true, }, "list": [ { "hidden":false, "name" : "cmd", "commandline" : "cmd.exe", "guid": "{0caa0dad-35be-5f56-a8ff-afceeeaa6101}", "startingDirectory" : "%USERPROFILE%", "opacity" : 20 }, { "name" : "the COOLER cmd", "commandline" : "c:\\windows\\system32\\cmd.exe", "startingDirectory" : "%USERPROFILE%", }, { "name" : "the sneaky cmd", "commandline" : "c:\\windows\\system32\\cmd.exe /k echo sneaky sneaks", "startingDirectory" : "%USERPROFILE%", }, { "name": "elevated cmd", "commandline": "cmd.exe /k echo This profile is always elevated", "startingDirectory" : "well this is garbage", "elevate": true, "background": "#9C1C0C", "tabColor": "#9C1C0C", "colorScheme": "Desert" }, { "name": "unelevated cmd", "commandline": "cmd.exe /k echo This profile is just as elevated as you started with", "elevate": false, "background": "#1C0C9C", "tabColor": "#1C0C9C", "colorScheme": "DotGov", "useAcrylic": true }, ] ```
Also try: * `wtd nt -p "elevated cmd" ; sp -p "elevated cmd"` * `wtd nt -p "elevated cmd" ; nt -p "elevated cmd"` This was merged manually via ``` git diff dev/migrie/f/non-terminal-content-elevation-warning dev/migrie/f/632-on-warning-dialog > ..\632.patch git apply ..\632.patch --ignore-whitespace --reject ``` --- OpenConsole.sln | 49 +++- doc/cascadia/profiles.schema.json | 12 +- .../CascadiaPackage/CascadiaPackage.wapproj | 3 + src/cascadia/ElevateShim/elevate-shim.cpp | 53 ++++ src/cascadia/ElevateShim/elevate-shim.rc | 69 +++++ src/cascadia/ElevateShim/elevate-shim.vcxproj | 34 +++ src/cascadia/ElevateShim/resource.h | 16 ++ .../LocalTests_TerminalApp/SettingsTests.cpp | 262 ++++++++++++++++++ src/cascadia/TerminalApp/TabManagement.cpp | 18 +- src/cascadia/TerminalApp/TerminalPage.cpp | 148 +++++++++- src/cascadia/TerminalApp/TerminalPage.h | 5 + .../TerminalSettingsModel/ActionArgs.h | 9 +- .../TerminalSettingsModel/ActionArgs.idl | 11 +- .../TerminalSettingsModel/MTSMSettings.h | 3 +- .../TerminalSettingsModel/Profile.idl | 2 + .../TerminalSettings.cpp | 10 + .../TerminalSettingsModel/TerminalSettings.h | 2 + .../TerminalSettings.idl | 2 + 18 files changed, 689 insertions(+), 19 deletions(-) create mode 100644 src/cascadia/ElevateShim/elevate-shim.cpp create mode 100644 src/cascadia/ElevateShim/elevate-shim.rc create mode 100644 src/cascadia/ElevateShim/elevate-shim.vcxproj create mode 100644 src/cascadia/ElevateShim/resource.h diff --git a/OpenConsole.sln b/OpenConsole.sln index 8e1ffdb2c1f..510b7a1a023 100644 --- a/OpenConsole.sln +++ b/OpenConsole.sln @@ -334,6 +334,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "WpfTerminalTestNetCore", "s EndProject Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "wt", "src\cascadia\wt\wt.vcxproj", "{506FD703-BAA7-4F6E-9361-64F550EC8FCA}" EndProject +Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "elevate-shim", "src\cascadia\ElevateShim\elevate-shim.vcxproj", "{416FD703-BAA7-4F6E-9361-64F550EC8FCA}" +EndProject Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "Microsoft.Terminal.Settings.Editor", "src\cascadia\TerminalSettingsEditor\Microsoft.Terminal.Settings.Editor.vcxproj", "{CA5CAD1A-0B5E-45C3-96A8-BB496BFE4E32}" ProjectSection(ProjectDependencies) = postProject {CA5CAD1A-082C-4476-9F33-94B339494076} = {CA5CAD1A-082C-4476-9F33-94B339494076} @@ -398,6 +400,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "WindowsTerminal.UIA.Tests", EndProject Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "api-ms-win-core-synch-l1-2-0", "src\api-ms-win-core-synch-l1-2-0\api-ms-win-core-synch-l1-2-0.vcxproj", "{9CF74355-F018-4C19-81AD-9DC6B7F2C6F5}" EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Utils", "Utils", "{61901E80-E97D-4D61-A9BB-E8F2FDA8B40C}" +EndProject Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "RendererAtlas", "src\renderer\atlas\atlas.vcxproj", "{8222900C-8B6C-452A-91AC-BE95DB04B95F}" EndProject Global @@ -2759,6 +2763,43 @@ Global {506FD703-BAA7-4F6E-9361-64F550EC8FCA}.Release|x64.Build.0 = Release|x64 {506FD703-BAA7-4F6E-9361-64F550EC8FCA}.Release|x86.ActiveCfg = Release|Win32 {506FD703-BAA7-4F6E-9361-64F550EC8FCA}.Release|x86.Build.0 = Release|Win32 + {416FD703-BAA7-4F6E-9361-64F550EC8FCA}.AuditMode|Any CPU.ActiveCfg = AuditMode|Win32 + {416FD703-BAA7-4F6E-9361-64F550EC8FCA}.AuditMode|ARM.ActiveCfg = AuditMode|Win32 + {416FD703-BAA7-4F6E-9361-64F550EC8FCA}.AuditMode|ARM64.ActiveCfg = AuditMode|ARM64 + {416FD703-BAA7-4F6E-9361-64F550EC8FCA}.AuditMode|ARM64.Build.0 = AuditMode|ARM64 + {416FD703-BAA7-4F6E-9361-64F550EC8FCA}.AuditMode|DotNet_x64Test.ActiveCfg = AuditMode|Win32 + {416FD703-BAA7-4F6E-9361-64F550EC8FCA}.AuditMode|DotNet_x86Test.ActiveCfg = AuditMode|Win32 + {416FD703-BAA7-4F6E-9361-64F550EC8FCA}.AuditMode|x64.ActiveCfg = AuditMode|x64 + {416FD703-BAA7-4F6E-9361-64F550EC8FCA}.AuditMode|x64.Build.0 = AuditMode|x64 + {416FD703-BAA7-4F6E-9361-64F550EC8FCA}.AuditMode|x86.ActiveCfg = AuditMode|Win32 + {416FD703-BAA7-4F6E-9361-64F550EC8FCA}.AuditMode|x86.Build.0 = AuditMode|Win32 + {416FD703-BAA7-4F6E-9361-64F550EC8FCA}.Debug|Any CPU.ActiveCfg = Debug|Win32 + {416FD703-BAA7-4F6E-9361-64F550EC8FCA}.Debug|ARM.ActiveCfg = Debug|Win32 + {416FD703-BAA7-4F6E-9361-64F550EC8FCA}.Debug|ARM64.ActiveCfg = Debug|ARM64 + {416FD703-BAA7-4F6E-9361-64F550EC8FCA}.Debug|ARM64.Build.0 = Debug|ARM64 + {416FD703-BAA7-4F6E-9361-64F550EC8FCA}.Debug|DotNet_x64Test.ActiveCfg = Debug|Win32 + {416FD703-BAA7-4F6E-9361-64F550EC8FCA}.Debug|DotNet_x86Test.ActiveCfg = Debug|Win32 + {416FD703-BAA7-4F6E-9361-64F550EC8FCA}.Debug|x64.ActiveCfg = Debug|x64 + {416FD703-BAA7-4F6E-9361-64F550EC8FCA}.Debug|x64.Build.0 = Debug|x64 + {416FD703-BAA7-4F6E-9361-64F550EC8FCA}.Debug|x86.ActiveCfg = Debug|Win32 + {416FD703-BAA7-4F6E-9361-64F550EC8FCA}.Debug|x86.Build.0 = Debug|Win32 + {416FD703-BAA7-4F6E-9361-64F550EC8FCA}.Fuzzing|Any CPU.ActiveCfg = Fuzzing|Win32 + {416FD703-BAA7-4F6E-9361-64F550EC8FCA}.Fuzzing|ARM.ActiveCfg = Fuzzing|Win32 + {416FD703-BAA7-4F6E-9361-64F550EC8FCA}.Fuzzing|ARM64.ActiveCfg = Fuzzing|ARM64 + {416FD703-BAA7-4F6E-9361-64F550EC8FCA}.Fuzzing|DotNet_x64Test.ActiveCfg = Fuzzing|Win32 + {416FD703-BAA7-4F6E-9361-64F550EC8FCA}.Fuzzing|DotNet_x86Test.ActiveCfg = Fuzzing|Win32 + {416FD703-BAA7-4F6E-9361-64F550EC8FCA}.Fuzzing|x64.ActiveCfg = Fuzzing|x64 + {416FD703-BAA7-4F6E-9361-64F550EC8FCA}.Fuzzing|x86.ActiveCfg = Fuzzing|Win32 + {416FD703-BAA7-4F6E-9361-64F550EC8FCA}.Release|Any CPU.ActiveCfg = Release|Win32 + {416FD703-BAA7-4F6E-9361-64F550EC8FCA}.Release|ARM.ActiveCfg = Release|Win32 + {416FD703-BAA7-4F6E-9361-64F550EC8FCA}.Release|ARM64.ActiveCfg = Release|ARM64 + {416FD703-BAA7-4F6E-9361-64F550EC8FCA}.Release|ARM64.Build.0 = Release|ARM64 + {416FD703-BAA7-4F6E-9361-64F550EC8FCA}.Release|DotNet_x64Test.ActiveCfg = Release|Win32 + {416FD703-BAA7-4F6E-9361-64F550EC8FCA}.Release|DotNet_x86Test.ActiveCfg = Release|Win32 + {416FD703-BAA7-4F6E-9361-64F550EC8FCA}.Release|x64.ActiveCfg = Release|x64 + {416FD703-BAA7-4F6E-9361-64F550EC8FCA}.Release|x64.Build.0 = Release|x64 + {416FD703-BAA7-4F6E-9361-64F550EC8FCA}.Release|x86.ActiveCfg = Release|Win32 + {416FD703-BAA7-4F6E-9361-64F550EC8FCA}.Release|x86.Build.0 = Release|Win32 {CA5CAD1A-0B5E-45C3-96A8-BB496BFE4E32}.AuditMode|Any CPU.ActiveCfg = Release|x64 {CA5CAD1A-0B5E-45C3-96A8-BB496BFE4E32}.AuditMode|Any CPU.Build.0 = Release|x64 {CA5CAD1A-0B5E-45C3-96A8-BB496BFE4E32}.AuditMode|Any CPU.Deploy.0 = Release|x64 @@ -3399,7 +3440,7 @@ Global {CA5CAD1A-9A12-429C-B551-8562EC954746} = {59840756-302F-44DF-AA47-441A9D673202} {CA5CAD1A-B11C-4DDB-A4FE-C3AFAE9B5506} = {BDB237B6-1D1D-400F-84CC-40A58FA59C8E} {48D21369-3D7B-4431-9967-24E81292CF63} = {05500DEF-2294-41E3-AF9A-24E580B82836} - {CA5CAD1A-039A-4929-BA2A-8BEB2E4106FE} = {59840756-302F-44DF-AA47-441A9D673202} + {CA5CAD1A-039A-4929-BA2A-8BEB2E4106FE} = {61901E80-E97D-4D61-A9BB-E8F2FDA8B40C} {58A03BB2-DF5A-4B66-91A0-7EF3BA01269A} = {E8F24881-5E37-4362-B191-A3BA0ED7F4EB} {A22EC5F6-7851-4B88-AC52-47249D437A52} = {E8F24881-5E37-4362-B191-A3BA0ED7F4EB} {A021EDFF-45C8-4DC2-BEF7-36E1B3B8CFE8} = {BDB237B6-1D1D-400F-84CC-40A58FA59C8E} @@ -3411,10 +3452,11 @@ Global {D3EF7B96-CD5E-47C9-B9A9-136259563033} = {04170EEF-983A-4195-BFEF-2321E5E38A1E} {95B136F9-B238-490C-A7C5-5843C1FECAC4} = {05500DEF-2294-41E3-AF9A-24E580B82836} {024052DE-83FB-4653-AEA4-90790D29D5BD} = {E8F24881-5E37-4362-B191-A3BA0ED7F4EB} - {067F0A06-FCB7-472C-96E9-B03B54E8E18D} = {59840756-302F-44DF-AA47-441A9D673202} + {067F0A06-FCB7-472C-96E9-B03B54E8E18D} = {61901E80-E97D-4D61-A9BB-E8F2FDA8B40C} {6BAE5851-50D5-4934-8D5E-30361A8A40F3} = {81C352DB-1818-45B7-A284-18E259F1CC87} {1588FD7C-241E-4E7D-9113-43735F3E6BAD} = {4DAF0299-495E-4CD1-A982-9BAC16A45932} - {506FD703-BAA7-4F6E-9361-64F550EC8FCA} = {59840756-302F-44DF-AA47-441A9D673202} + {506FD703-BAA7-4F6E-9361-64F550EC8FCA} = {61901E80-E97D-4D61-A9BB-E8F2FDA8B40C} + {416FD703-BAA7-4F6E-9361-64F550EC8FCA} = {61901E80-E97D-4D61-A9BB-E8F2FDA8B40C} {CA5CAD1A-0B5E-45C3-96A8-BB496BFE4E32} = {77875138-BB08-49F9-8BB1-409C2150E0E1} {CA5CAD1A-D7EC-4107-B7C6-79CB77AE2907} = {77875138-BB08-49F9-8BB1-409C2150E0E1} {CA5CAD1A-082C-4476-9F33-94B339494076} = {77875138-BB08-49F9-8BB1-409C2150E0E1} @@ -3433,6 +3475,7 @@ Global {C323DAEE-B307-4C7B-ACE5-7293CBEFCB5B} = {BDB237B6-1D1D-400F-84CC-40A58FA59C8E} {F19DACD5-0C6E-40DC-B6E4-767A3200542C} = {BDB237B6-1D1D-400F-84CC-40A58FA59C8E} {9CF74355-F018-4C19-81AD-9DC6B7F2C6F5} = {89CDCC5C-9F53-4054-97A4-639D99F169CD} + {61901E80-E97D-4D61-A9BB-E8F2FDA8B40C} = {59840756-302F-44DF-AA47-441A9D673202} {8222900C-8B6C-452A-91AC-BE95DB04B95F} = {05500DEF-2294-41E3-AF9A-24E580B82836} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution diff --git a/doc/cascadia/profiles.schema.json b/doc/cascadia/profiles.schema.json index 00fbcb1509d..4d4425fd3f2 100644 --- a/doc/cascadia/profiles.schema.json +++ b/doc/cascadia/profiles.schema.json @@ -469,7 +469,12 @@ "colorScheme": { "description": "The name of a color scheme to use, instead of the one specified by the profile", "type": "string" - } + }, + "elevate": { + "type": "boolean", + "default": false, + "description": "This will override the profile's `elevate` setting." + }, }, "type": "object" }, @@ -1951,6 +1956,11 @@ ], "type": "string" }, + "elevate": { + "type": "boolean", + "default": false, + "description": "When true, this profile should always open in an elevated context. If the window isn't running as an Administrator, then a new elevated window will be created." + }, "experimental.retroTerminalEffect": { "description": "When set to true, enable retro terminal effects. This is an experimental feature, and its continued existence is not guaranteed.", "type": "boolean" diff --git a/src/cascadia/CascadiaPackage/CascadiaPackage.wapproj b/src/cascadia/CascadiaPackage/CascadiaPackage.wapproj index 6e207c7bf10..48d8caffdf9 100644 --- a/src/cascadia/CascadiaPackage/CascadiaPackage.wapproj +++ b/src/cascadia/CascadiaPackage/CascadiaPackage.wapproj @@ -78,6 +78,9 @@ {9CBD7DFA-1754-4A9D-93D7-857A9D17CB1B} + + {416fd703-baa7-4f6e-9361-64f550ec8fca} + {71CC9D78-BA29-4D93-946F-BEF5D9A3A6EF} diff --git a/src/cascadia/ElevateShim/elevate-shim.cpp b/src/cascadia/ElevateShim/elevate-shim.cpp new file mode 100644 index 00000000000..e9b4d859cfb --- /dev/null +++ b/src/cascadia/ElevateShim/elevate-shim.cpp @@ -0,0 +1,53 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +#include +#include + +#define WIN32_LEAN_AND_MEAN +#include +#include +#include +#include +#include + +// BODGY +// +// If we try to do this in the Terminal itself, then there's a bunch of weird +// things that can go wrong and prevent the elevated Terminal window from +// getting created. Specifically, if the origin Terminal exits right away after +// spawning the elevated WT, then ShellExecute might not successfully complete +// the elevation. What's even more, the Terminal will mysteriously crash +// somewhere in XAML land. +// +// To mitigate this, the Terminal will call into us with the commandline it +// wants elevated. We'll hang around until ShellExecute is finished, so that the +// process can successfully elevate. + +#pragma warning(suppress : 26461) // we can't change the signature of wWinMain +int __stdcall wWinMain(HINSTANCE, HINSTANCE, LPWSTR pCmdLine, int) +{ + // All of the args passed to us (something like `new-tab -p {guid}`) are in + // pCmdLine + + // Get the path to WindowsTerminal.exe, which should live next to us. + std::filesystem::path module{ wil::GetModuleFileNameW(nullptr) }; + // Swap elevate-shim.exe for WindowsTerminal.exe + module.replace_filename(L"WindowsTerminal.exe"); + + // Go! + + // disable warnings from SHELLEXECUTEINFOW struct. We can't fix that. +#pragma warning(push) +#pragma warning(disable : 26476) // Macro uses naked union over variant. + SHELLEXECUTEINFOW seInfo{ 0 }; +#pragma warning(pop) + + seInfo.cbSize = sizeof(seInfo); + seInfo.fMask = SEE_MASK_DEFAULT; + seInfo.lpVerb = L"runas"; // This asks the shell to elevate the process + seInfo.lpFile = module.c_str(); // This is `...\WindowsTerminal.exe` + seInfo.lpParameters = pCmdLine; // This is `new-tab -p {guid}` + seInfo.nShow = SW_SHOWNORMAL; + LOG_IF_WIN32_BOOL_FALSE(ShellExecuteExW(&seInfo)); +} diff --git a/src/cascadia/ElevateShim/elevate-shim.rc b/src/cascadia/ElevateShim/elevate-shim.rc new file mode 100644 index 00000000000..20d7e068e25 --- /dev/null +++ b/src/cascadia/ElevateShim/elevate-shim.rc @@ -0,0 +1,69 @@ +// Microsoft Visual C++ generated resource script. +// +#include "resource.h" + +#define APSTUDIO_READONLY_SYMBOLS +///////////////////////////////////////////////////////////////////////////// +// +// Generated from the TEXTINCLUDE 2 resource. +// +#include "winres.h" +///////////////////////////////////////////////////////////////////////////// +#undef APSTUDIO_READONLY_SYMBOLS + +///////////////////////////////////////////////////////////////////////////// +// English (United States) resources + +#if !defined(AFX_RESOURCE_DLL) || defined(AFX_TARG_ENU) +LANGUAGE LANG_ENGLISH, SUBLANG_ENGLISH_US +#pragma code_page(1252) + +#ifdef APSTUDIO_INVOKED +///////////////////////////////////////////////////////////////////////////// +// +// TEXTINCLUDE +// + +1 TEXTINCLUDE +BEGIN + "resource.h\0" +END + +2 TEXTINCLUDE +BEGIN + "#include ""winres.h""\0" +END + +3 TEXTINCLUDE +BEGIN + "\r\n" + "\0" +END + +#endif // APSTUDIO_INVOKED + + +///////////////////////////////////////////////////////////////////////////// +// +// Icon +// + +// Icon with lowest ID value placed first to ensure application icon +// remains consistent on all systems. +IDI_APPICON ICON "..\\..\\..\\res\\terminal.ico" + +#endif // English (United States) resources +///////////////////////////////////////////////////////////////////////////// + + + +#ifndef APSTUDIO_INVOKED +///////////////////////////////////////////////////////////////////////////// +// +// Generated from the TEXTINCLUDE 3 resource. +// + + +///////////////////////////////////////////////////////////////////////////// +#endif // not APSTUDIO_INVOKED + diff --git a/src/cascadia/ElevateShim/elevate-shim.vcxproj b/src/cascadia/ElevateShim/elevate-shim.vcxproj new file mode 100644 index 00000000000..f81353ad368 --- /dev/null +++ b/src/cascadia/ElevateShim/elevate-shim.vcxproj @@ -0,0 +1,34 @@ + + + + {416fd703-baa7-4f6e-9361-64f550ec8fca} + Win32Proj + elevate-shim + elevate-shim + elevate-shim + Application + + + + + + + + + NotUsing + + + + + + + + + + + + + onecore.lib + + + diff --git a/src/cascadia/ElevateShim/resource.h b/src/cascadia/ElevateShim/resource.h new file mode 100644 index 00000000000..62d8839e293 --- /dev/null +++ b/src/cascadia/ElevateShim/resource.h @@ -0,0 +1,16 @@ +//{{NO_DEPENDENCIES}} +// Microsoft Visual C++ generated include file. +// Used by wt.rc +// +#define IDI_APPICON 101 + +// Next default values for new objects +// +#ifdef APSTUDIO_INVOKED +#ifndef APSTUDIO_READONLY_SYMBOLS +#define _APS_NEXT_RESOURCE_VALUE 102 +#define _APS_NEXT_COMMAND_VALUE 40001 +#define _APS_NEXT_CONTROL_VALUE 1001 +#define _APS_NEXT_SYMED_VALUE 101 +#endif +#endif diff --git a/src/cascadia/LocalTests_TerminalApp/SettingsTests.cpp b/src/cascadia/LocalTests_TerminalApp/SettingsTests.cpp index f2c6fac9d81..a3dcbb45f4e 100644 --- a/src/cascadia/LocalTests_TerminalApp/SettingsTests.cpp +++ b/src/cascadia/LocalTests_TerminalApp/SettingsTests.cpp @@ -69,6 +69,8 @@ namespace TerminalAppLocalTests TEST_METHOD(TestIterableColorSchemeCommands); + TEST_METHOD(TestElevateArg); + TEST_CLASS_SETUP(ClassSetup) { return true; @@ -1267,4 +1269,264 @@ namespace TerminalAppLocalTests } } + void SettingsTests::TestElevateArg() + { + static constexpr std::wstring_view settingsJson{ LR"( + { + "defaultProfile": "{6239a42c-0000-49a3-80bd-e8fdd045185c}", + "profiles": [ + { + "name": "profile0", + "guid": "{6239a42c-0000-49a3-80bd-e8fdd045185c}", + "commandline": "cmd.exe" + }, + { + "name": "profile1", + "guid": "{6239a42c-1111-49a3-80bd-e8fdd045185c}", + "elevate": true, + "commandline": "pwsh.exe" + }, + { + "name": "profile2", + "guid": "{6239a42c-2222-49a3-80bd-e8fdd045185c}", + "elevate": false, + "commandline": "wsl.exe" + } + ], + "keybindings": [ + { "keys": ["ctrl+a"], "command": { "action": "newTab", "profile": "profile0" } }, + { "keys": ["ctrl+b"], "command": { "action": "newTab", "profile": "profile1" } }, + { "keys": ["ctrl+c"], "command": { "action": "newTab", "profile": "profile2" } }, + + { "keys": ["ctrl+d"], "command": { "action": "newTab", "profile": "profile0", "elevate": false } }, + { "keys": ["ctrl+e"], "command": { "action": "newTab", "profile": "profile1", "elevate": false } }, + { "keys": ["ctrl+f"], "command": { "action": "newTab", "profile": "profile2", "elevate": false } }, + + { "keys": ["ctrl+g"], "command": { "action": "newTab", "profile": "profile0", "elevate": true } }, + { "keys": ["ctrl+h"], "command": { "action": "newTab", "profile": "profile1", "elevate": true } }, + { "keys": ["ctrl+i"], "command": { "action": "newTab", "profile": "profile2", "elevate": true } }, + ] + })" }; + + const winrt::guid guid0{ ::Microsoft::Console::Utils::GuidFromString(L"{6239a42c-0000-49a3-80bd-e8fdd045185c}") }; + const winrt::guid guid1{ ::Microsoft::Console::Utils::GuidFromString(L"{6239a42c-1111-49a3-80bd-e8fdd045185c}") }; + const winrt::guid guid2{ ::Microsoft::Console::Utils::GuidFromString(L"{6239a42c-2222-49a3-80bd-e8fdd045185c}") }; + + CascadiaSettings settings{ settingsJson, {} }; + + auto keymap = settings.GlobalSettings().ActionMap(); + VERIFY_ARE_EQUAL(3u, settings.ActiveProfiles().Size()); + + const auto profile2Guid = settings.ActiveProfiles().GetAt(2).Guid(); + VERIFY_ARE_NOT_EQUAL(winrt::guid{}, profile2Guid); + + VERIFY_ARE_EQUAL(9u, keymap.KeyBindings().Size()); + + { + Log::Comment(L"profile.elevate=omitted, action.elevate=nullopt: don't auto elevate"); + + KeyChord kc{ true, false, false, false, static_cast('A'), 0 }; + auto actionAndArgs = TestUtils::GetActionAndArgs(keymap, kc); + VERIFY_ARE_EQUAL(ShortcutAction::NewTab, actionAndArgs.Action()); + const auto& realArgs = actionAndArgs.Args().try_as(); + VERIFY_IS_NOT_NULL(realArgs); + // Verify the args have the expected value + VERIFY_IS_NOT_NULL(realArgs.TerminalArgs()); + VERIFY_IS_TRUE(realArgs.TerminalArgs().Commandline().empty()); + VERIFY_IS_TRUE(realArgs.TerminalArgs().StartingDirectory().empty()); + VERIFY_IS_TRUE(realArgs.TerminalArgs().TabTitle().empty()); + VERIFY_IS_FALSE(realArgs.TerminalArgs().Profile().empty()); + VERIFY_ARE_EQUAL(L"profile0", realArgs.TerminalArgs().Profile()); + VERIFY_IS_NULL(realArgs.TerminalArgs().Elevate()); + + const auto termSettingsResult = TerminalSettings::CreateWithNewTerminalArgs(settings, realArgs.TerminalArgs(), nullptr); + const auto termSettings = termSettingsResult.DefaultSettings(); + VERIFY_ARE_EQUAL(L"cmd.exe", termSettings.Commandline()); + VERIFY_ARE_EQUAL(false, termSettings.Elevate()); + } + { + Log::Comment(L"profile.elevate=true, action.elevate=nullopt: DO auto elevate"); + + KeyChord kc{ true, false, false, false, static_cast('B'), 0 }; + auto actionAndArgs = TestUtils::GetActionAndArgs(keymap, kc); + VERIFY_ARE_EQUAL(ShortcutAction::NewTab, actionAndArgs.Action()); + const auto& realArgs = actionAndArgs.Args().try_as(); + VERIFY_IS_NOT_NULL(realArgs); + // Verify the args have the expected value + VERIFY_IS_NOT_NULL(realArgs.TerminalArgs()); + VERIFY_IS_TRUE(realArgs.TerminalArgs().Commandline().empty()); + VERIFY_IS_TRUE(realArgs.TerminalArgs().StartingDirectory().empty()); + VERIFY_IS_TRUE(realArgs.TerminalArgs().TabTitle().empty()); + VERIFY_IS_FALSE(realArgs.TerminalArgs().Profile().empty()); + VERIFY_ARE_EQUAL(L"profile1", realArgs.TerminalArgs().Profile()); + VERIFY_IS_NULL(realArgs.TerminalArgs().Elevate()); + + const auto termSettingsResult = TerminalSettings::CreateWithNewTerminalArgs(settings, realArgs.TerminalArgs(), nullptr); + const auto termSettings = termSettingsResult.DefaultSettings(); + VERIFY_ARE_EQUAL(L"pwsh.exe", termSettings.Commandline()); + VERIFY_ARE_EQUAL(true, termSettings.Elevate()); + } + { + Log::Comment(L"profile.elevate=false, action.elevate=nullopt: don't auto elevate"); + + KeyChord kc{ true, false, false, false, static_cast('C'), 0 }; + auto actionAndArgs = TestUtils::GetActionAndArgs(keymap, kc); + VERIFY_ARE_EQUAL(ShortcutAction::NewTab, actionAndArgs.Action()); + const auto& realArgs = actionAndArgs.Args().try_as(); + VERIFY_IS_NOT_NULL(realArgs); + // Verify the args have the expected value + VERIFY_IS_NOT_NULL(realArgs.TerminalArgs()); + VERIFY_IS_TRUE(realArgs.TerminalArgs().Commandline().empty()); + VERIFY_IS_TRUE(realArgs.TerminalArgs().StartingDirectory().empty()); + VERIFY_IS_TRUE(realArgs.TerminalArgs().TabTitle().empty()); + VERIFY_IS_FALSE(realArgs.TerminalArgs().Profile().empty()); + VERIFY_ARE_EQUAL(L"profile2", realArgs.TerminalArgs().Profile()); + VERIFY_IS_NULL(realArgs.TerminalArgs().Elevate()); + + const auto termSettingsResult = TerminalSettings::CreateWithNewTerminalArgs(settings, realArgs.TerminalArgs(), nullptr); + const auto termSettings = termSettingsResult.DefaultSettings(); + VERIFY_ARE_EQUAL(L"wsl.exe", termSettings.Commandline()); + VERIFY_ARE_EQUAL(false, termSettings.Elevate()); + } + + { + Log::Comment(L"profile.elevate=omitted, action.elevate=false: don't auto elevate"); + + KeyChord kc{ true, false, false, false, static_cast('D'), 0 }; + auto actionAndArgs = TestUtils::GetActionAndArgs(keymap, kc); + VERIFY_ARE_EQUAL(ShortcutAction::NewTab, actionAndArgs.Action()); + const auto& realArgs = actionAndArgs.Args().try_as(); + VERIFY_IS_NOT_NULL(realArgs); + // Verify the args have the expected value + VERIFY_IS_NOT_NULL(realArgs.TerminalArgs()); + VERIFY_IS_TRUE(realArgs.TerminalArgs().Commandline().empty()); + VERIFY_IS_TRUE(realArgs.TerminalArgs().StartingDirectory().empty()); + VERIFY_IS_TRUE(realArgs.TerminalArgs().TabTitle().empty()); + VERIFY_IS_FALSE(realArgs.TerminalArgs().Profile().empty()); + VERIFY_ARE_EQUAL(L"profile0", realArgs.TerminalArgs().Profile()); + VERIFY_IS_NOT_NULL(realArgs.TerminalArgs().Elevate()); + VERIFY_IS_FALSE(realArgs.TerminalArgs().Elevate().Value()); + + const auto termSettingsResult = TerminalSettings::CreateWithNewTerminalArgs(settings, realArgs.TerminalArgs(), nullptr); + const auto termSettings = termSettingsResult.DefaultSettings(); + VERIFY_ARE_EQUAL(L"cmd.exe", termSettings.Commandline()); + VERIFY_ARE_EQUAL(false, termSettings.Elevate()); + } + { + Log::Comment(L"profile.elevate=true, action.elevate=false: don't auto elevate"); + + KeyChord kc{ true, false, false, false, static_cast('E'), 0 }; + auto actionAndArgs = TestUtils::GetActionAndArgs(keymap, kc); + VERIFY_ARE_EQUAL(ShortcutAction::NewTab, actionAndArgs.Action()); + const auto& realArgs = actionAndArgs.Args().try_as(); + VERIFY_IS_NOT_NULL(realArgs); + // Verify the args have the expected value + VERIFY_IS_NOT_NULL(realArgs.TerminalArgs()); + VERIFY_IS_TRUE(realArgs.TerminalArgs().Commandline().empty()); + VERIFY_IS_TRUE(realArgs.TerminalArgs().StartingDirectory().empty()); + VERIFY_IS_TRUE(realArgs.TerminalArgs().TabTitle().empty()); + VERIFY_IS_FALSE(realArgs.TerminalArgs().Profile().empty()); + VERIFY_ARE_EQUAL(L"profile1", realArgs.TerminalArgs().Profile()); + VERIFY_IS_NOT_NULL(realArgs.TerminalArgs().Elevate()); + VERIFY_IS_FALSE(realArgs.TerminalArgs().Elevate().Value()); + + const auto termSettingsResult = TerminalSettings::CreateWithNewTerminalArgs(settings, realArgs.TerminalArgs(), nullptr); + const auto termSettings = termSettingsResult.DefaultSettings(); + VERIFY_ARE_EQUAL(L"pwsh.exe", termSettings.Commandline()); + VERIFY_ARE_EQUAL(false, termSettings.Elevate()); + } + { + Log::Comment(L"profile.elevate=false, action.elevate=false: don't auto elevate"); + + KeyChord kc{ true, false, false, false, static_cast('F'), 0 }; + auto actionAndArgs = TestUtils::GetActionAndArgs(keymap, kc); + VERIFY_ARE_EQUAL(ShortcutAction::NewTab, actionAndArgs.Action()); + const auto& realArgs = actionAndArgs.Args().try_as(); + VERIFY_IS_NOT_NULL(realArgs); + // Verify the args have the expected value + VERIFY_IS_NOT_NULL(realArgs.TerminalArgs()); + VERIFY_IS_TRUE(realArgs.TerminalArgs().Commandline().empty()); + VERIFY_IS_TRUE(realArgs.TerminalArgs().StartingDirectory().empty()); + VERIFY_IS_TRUE(realArgs.TerminalArgs().TabTitle().empty()); + VERIFY_IS_FALSE(realArgs.TerminalArgs().Profile().empty()); + VERIFY_ARE_EQUAL(L"profile2", realArgs.TerminalArgs().Profile()); + VERIFY_IS_NOT_NULL(realArgs.TerminalArgs().Elevate()); + VERIFY_IS_FALSE(realArgs.TerminalArgs().Elevate().Value()); + + const auto termSettingsResult = TerminalSettings::CreateWithNewTerminalArgs(settings, realArgs.TerminalArgs(), nullptr); + const auto termSettings = termSettingsResult.DefaultSettings(); + VERIFY_ARE_EQUAL(L"wsl.exe", termSettings.Commandline()); + VERIFY_ARE_EQUAL(false, termSettings.Elevate()); + } + + { + Log::Comment(L"profile.elevate=omitted, action.elevate=true: DO auto elevate"); + + KeyChord kc{ true, false, false, false, static_cast('G'), 0 }; + auto actionAndArgs = TestUtils::GetActionAndArgs(keymap, kc); + VERIFY_ARE_EQUAL(ShortcutAction::NewTab, actionAndArgs.Action()); + const auto& realArgs = actionAndArgs.Args().try_as(); + VERIFY_IS_NOT_NULL(realArgs); + // Verify the args have the expected value + VERIFY_IS_NOT_NULL(realArgs.TerminalArgs()); + VERIFY_IS_TRUE(realArgs.TerminalArgs().Commandline().empty()); + VERIFY_IS_TRUE(realArgs.TerminalArgs().StartingDirectory().empty()); + VERIFY_IS_TRUE(realArgs.TerminalArgs().TabTitle().empty()); + VERIFY_IS_FALSE(realArgs.TerminalArgs().Profile().empty()); + VERIFY_ARE_EQUAL(L"profile0", realArgs.TerminalArgs().Profile()); + VERIFY_IS_NOT_NULL(realArgs.TerminalArgs().Elevate()); + VERIFY_IS_TRUE(realArgs.TerminalArgs().Elevate().Value()); + + const auto termSettingsResult = TerminalSettings::CreateWithNewTerminalArgs(settings, realArgs.TerminalArgs(), nullptr); + const auto termSettings = termSettingsResult.DefaultSettings(); + VERIFY_ARE_EQUAL(L"cmd.exe", termSettings.Commandline()); + VERIFY_ARE_EQUAL(true, termSettings.Elevate()); + } + { + Log::Comment(L"profile.elevate=true, action.elevate=true: DO auto elevate"); + KeyChord kc{ true, false, false, false, static_cast('H'), 0 }; + auto actionAndArgs = TestUtils::GetActionAndArgs(keymap, kc); + VERIFY_ARE_EQUAL(ShortcutAction::NewTab, actionAndArgs.Action()); + const auto& realArgs = actionAndArgs.Args().try_as(); + VERIFY_IS_NOT_NULL(realArgs); + // Verify the args have the expected value + VERIFY_IS_NOT_NULL(realArgs.TerminalArgs()); + VERIFY_IS_TRUE(realArgs.TerminalArgs().Commandline().empty()); + VERIFY_IS_TRUE(realArgs.TerminalArgs().StartingDirectory().empty()); + VERIFY_IS_TRUE(realArgs.TerminalArgs().TabTitle().empty()); + VERIFY_IS_FALSE(realArgs.TerminalArgs().Profile().empty()); + VERIFY_ARE_EQUAL(L"profile1", realArgs.TerminalArgs().Profile()); + VERIFY_IS_NOT_NULL(realArgs.TerminalArgs().Elevate()); + VERIFY_IS_TRUE(realArgs.TerminalArgs().Elevate().Value()); + + const auto termSettingsResult = TerminalSettings::CreateWithNewTerminalArgs(settings, realArgs.TerminalArgs(), nullptr); + const auto termSettings = termSettingsResult.DefaultSettings(); + VERIFY_ARE_EQUAL(L"pwsh.exe", termSettings.Commandline()); + VERIFY_ARE_EQUAL(true, termSettings.Elevate()); + } + { + Log::Comment(L"profile.elevate=false, action.elevate=true: DO auto elevate"); + + KeyChord kc{ true, false, false, false, static_cast('I'), 0 }; + auto actionAndArgs = TestUtils::GetActionAndArgs(keymap, kc); + VERIFY_ARE_EQUAL(ShortcutAction::NewTab, actionAndArgs.Action()); + const auto& realArgs = actionAndArgs.Args().try_as(); + VERIFY_IS_NOT_NULL(realArgs); + // Verify the args have the expected value + VERIFY_IS_NOT_NULL(realArgs.TerminalArgs()); + VERIFY_IS_TRUE(realArgs.TerminalArgs().Commandline().empty()); + VERIFY_IS_TRUE(realArgs.TerminalArgs().StartingDirectory().empty()); + VERIFY_IS_TRUE(realArgs.TerminalArgs().TabTitle().empty()); + VERIFY_IS_FALSE(realArgs.TerminalArgs().Profile().empty()); + VERIFY_ARE_EQUAL(L"profile2", realArgs.TerminalArgs().Profile()); + VERIFY_IS_NOT_NULL(realArgs.TerminalArgs().Elevate()); + VERIFY_IS_TRUE(realArgs.TerminalArgs().Elevate().Value()); + + const auto termSettingsResult = TerminalSettings::CreateWithNewTerminalArgs(settings, realArgs.TerminalArgs(), nullptr); + const auto termSettings = termSettingsResult.DefaultSettings(); + VERIFY_ARE_EQUAL(L"wsl.exe", termSettings.Commandline()); + VERIFY_ARE_EQUAL(true, termSettings.Elevate()); + } + } + } diff --git a/src/cascadia/TerminalApp/TabManagement.cpp b/src/cascadia/TerminalApp/TabManagement.cpp index 57becf6ab58..04d4260d941 100644 --- a/src/cascadia/TerminalApp/TabManagement.cpp +++ b/src/cascadia/TerminalApp/TabManagement.cpp @@ -68,6 +68,17 @@ namespace winrt::TerminalApp::implementation const auto profile{ _settings.GetProfileForArgs(newTerminalArgs) }; const auto settings{ TerminalSettings::CreateWithNewTerminalArgs(_settings, newTerminalArgs, *_bindings) }; + // Try to handle auto-elevation + if (_maybeElevate(newTerminalArgs, settings, profile)) + { + return S_OK; + } + // We can't go in the other direction (elevated->unelevated) + // unfortunately. This seems to be due to Centennial quirks. It works + // unpackaged, but not packaged. + // + // This call to _MakePane won't return nullptr, we already checked that + // case above with the _maybeElevate call. _CreateNewTabFromPane(_MakePane(newTerminalArgs, false, existingConnection)); const uint32_t tabCount = _tabs.Size(); @@ -240,8 +251,11 @@ namespace winrt::TerminalApp::implementation // - pane: The pane to use as the root. void TerminalPage::_CreateNewTabFromPane(std::shared_ptr pane) { - auto newTabImpl = winrt::make_self(pane); - _InitializeTab(newTabImpl); + if (pane) + { + auto newTabImpl = winrt::make_self(pane); + _InitializeTab(newTabImpl); + } } // Method Description: diff --git a/src/cascadia/TerminalApp/TerminalPage.cpp b/src/cascadia/TerminalApp/TerminalPage.cpp index 95c7b595cee..4d0e4584054 100644 --- a/src/cascadia/TerminalApp/TerminalPage.cpp +++ b/src/cascadia/TerminalApp/TerminalPage.cpp @@ -546,7 +546,24 @@ namespace winrt::TerminalApp::implementation void TerminalPage::_CompleteInitialization() { _startupState = StartupState::Initialized; - _InitializedHandlers(*this, nullptr); + + // GH#632 - It's possible that the user tried to create the terminal + // with only one tab, with only an elevated profile. If that happens, + // we'll create _another_ process to host the elevated version of that + // profile. This can happen from the jumplist, or if the default profile + // is `elevate:true`, or from the commandline. + // + // However, we need to make sure to close this window in that scenario. + // Since there aren't any _tabs_ in this window, we won't ever get a + // closed event. So do it manually. + if (_tabs.Size() == 0) + { + _LastTabClosedHandlers(*this, nullptr); + } + else + { + _InitializedHandlers(*this, nullptr); + } } // Method Description: @@ -862,6 +879,13 @@ namespace winrt::TerminalApp::implementation else { const auto newPane = _MakePane(newTerminalArgs); + // If the newTerminalArgs caused us to open an elevated window + // instead of creating a pane, it may have returned nullptr. Just do + // nothing then. + if (!newPane) + { + return; + } if (altPressed && !debugTap) { this->_SplitPane(SplitDirection::Automatic, @@ -1614,13 +1638,6 @@ namespace winrt::TerminalApp::implementation std::shared_ptr newPane) { const auto focusedTab{ _GetFocusedTabImpl() }; - - // Do nothing if no TerminalTab is focused - if (!focusedTab) - { - return; - } - _SplitPane(*focusedTab, splitDirection, splitSize, newPane); } @@ -1638,6 +1655,14 @@ namespace winrt::TerminalApp::implementation const float splitSize, std::shared_ptr newPane) { + // If the caller is calling us with the return value of _MakePane + // directly, it's possible that nullptr was returned, if the connections + // was supposed to be launched in an elevated window. In that case, do + // nothing here. We don't have a pane with which to create the split. + if (!newPane) + { + return; + } const float contentWidth = ::base::saturated_cast(_tabContent.ActualWidth()); const float contentHeight = ::base::saturated_cast(_tabContent.ActualHeight()); const winrt::Windows::Foundation::Size availableSpace{ contentWidth, contentHeight }; @@ -2240,7 +2265,13 @@ namespace winrt::TerminalApp::implementation // a duplicate of the currently focused pane // - existingConnection: optionally receives a connection from the outside // world instead of attempting to create one - std::shared_ptr TerminalPage::_MakePane(const NewTerminalArgs& newTerminalArgs, const bool duplicate, TerminalConnection::ITerminalConnection existingConnection) + // Return Value: + // - If the newTerminalArgs required us to open the pane as a new elevated + // connection, then we'll return nullptr. Otherwise, we'll return a new + // Pane for this connection. + std::shared_ptr TerminalPage::_MakePane(const NewTerminalArgs& newTerminalArgs, + const bool duplicate, + TerminalConnection::ITerminalConnection existingConnection) { TerminalSettingsCreateResult controlSettings{ nullptr }; Profile profile{ nullptr }; @@ -2271,6 +2302,12 @@ namespace winrt::TerminalApp::implementation controlSettings = TerminalSettings::CreateWithNewTerminalArgs(_settings, newTerminalArgs, *_bindings); } + // Try to handle auto-elevation + if (_maybeElevate(newTerminalArgs, controlSettings, profile)) + { + return nullptr; + } + auto connection = existingConnection ? existingConnection : _CreateConnectionFromSettings(profile, controlSettings.DefaultSettings()); if (existingConnection) { @@ -3447,6 +3484,98 @@ namespace winrt::TerminalApp::implementation return profile; } + // Function Description: + // - Helper to launch a new WT instance elevated. It'll do this by spawning + // a helper process, who will asking the shell to elevate the process for + // us. This might cause a UAC prompt. The elevation is performed on a + // background thread, as to not block the UI thread. + // Arguments: + // - newTerminalArgs: A NewTerminalArgs describing the terminal instance + // that should be spawned. The Profile should be filled in with the GUID + // of the profile we want to launch. + // Return Value: + // - + // Important: Don't take the param by reference, since we'll be doing work + // on another thread. + void TerminalPage::_OpenElevatedWT(NewTerminalArgs newTerminalArgs) + { + // BODGY + // + // We're going to construct the commandline we want, then toss it to a + // helper process called `elevate-shim.exe` that happens to live next to + // us. elevate-shim.exe will be the one to call ShellExecute with the + // args that we want (to elevate the given profile). + // + // We can't be the one to call ShellExecute ourselves. ShellExecute + // requires that the calling process stays alive until the child is + // spawned. However, in the case of something like `wt -p + // AlwaysElevateMe`, then the original WT will try to ShellExecute a new + // wt.exe (elevated) and immediately exit, preventing ShellExecute from + // successfully spawning the elevated WT. + + std::filesystem::path exePath = wil::GetModuleFileNameW(nullptr); + exePath.replace_filename(L"elevate-shim.exe"); + + // Build the commandline to pass to wt for this set of NewTerminalArgs + std::wstring cmdline{ + fmt::format(L"new-tab {}", newTerminalArgs.ToCommandline().c_str()) + }; + + wil::unique_process_information pi; + STARTUPINFOW si{}; + si.cb = sizeof(si); + + LOG_IF_WIN32_BOOL_FALSE(CreateProcessW(exePath.c_str(), + cmdline.data(), + nullptr, + nullptr, + FALSE, + 0, + nullptr, + nullptr, + &si, + &pi)); + + // TODO: GH#8592 - It may be useful to pop a Toast here in the original + // Terminal window informing the user that the tab was opened in a new + // window. + } + + // Method Description: + // - If the requested settings want us to elevate this new terminal + // instance, and we're not currently elevated, then open the new terminal + // as an elevated instance (using _OpenElevatedWT). Does nothing if we're + // already elevated, or if the control settings don't want to be elevated. + // Arguments: + // - newTerminalArgs: The NewTerminalArgs for this terminal instance + // - controlSettings: The constructed TerminalSettingsCreateResult for this Terminal instance + // - profile: The Profile we're using to launch this Terminal instance + // Return Value: + // - true iff we tossed this request to an elevated window. Callers can use + // this result to early-return if needed. + bool TerminalPage::_maybeElevate(const NewTerminalArgs& newTerminalArgs, + const TerminalSettingsCreateResult& controlSettings, + const Profile& profile) + { + // Try to handle auto-elevation + const bool requestedElevation = controlSettings.DefaultSettings().Elevate(); + const bool currentlyElevated = IsElevated(); + + // We aren't elevated, but we want to be. + if (requestedElevation && !currentlyElevated) + { + // Manually set the Profile of the NewTerminalArgs to the guid we've + // resolved to. If there was a profile in the NewTerminalArgs, this + // will be that profile's GUID. If there wasn't, then we'll use + // whatever the default profile's GUID is. + + newTerminalArgs.Profile(::Microsoft::Console::Utils::GuidToString(profile.Guid())); + _OpenElevatedWT(newTerminalArgs); + return true; + } + return false; + } + // Method Description: // - Handles the change of connection state. // If the connection state is failure show information bar suggesting to configure termination behavior @@ -3582,4 +3711,5 @@ namespace winrt::TerminalApp::implementation applicationState.DismissedMessages(std::move(messages)); } + } diff --git a/src/cascadia/TerminalApp/TerminalPage.h b/src/cascadia/TerminalApp/TerminalPage.h index 58688e74636..07da72e37f9 100644 --- a/src/cascadia/TerminalApp/TerminalPage.h +++ b/src/cascadia/TerminalApp/TerminalPage.h @@ -414,6 +414,11 @@ namespace winrt::TerminalApp::implementation winrt::Microsoft::Terminal::Settings::Model::Profile GetClosestProfileForDuplicationOfProfile(const winrt::Microsoft::Terminal::Settings::Model::Profile& profile) const noexcept; + bool _maybeElevate(const winrt::Microsoft::Terminal::Settings::Model::NewTerminalArgs& newTerminalArgs, + const winrt::Microsoft::Terminal::Settings::Model::TerminalSettingsCreateResult& controlSettings, + const winrt::Microsoft::Terminal::Settings::Model::Profile& profile); + void _OpenElevatedWT(winrt::Microsoft::Terminal::Settings::Model::NewTerminalArgs newTerminalArgs); + winrt::fire_and_forget _ConnectionStateChangedHandler(const winrt::Windows::Foundation::IInspectable& sender, const winrt::Windows::Foundation::IInspectable& args) const; void _CloseOnExitInfoDismissHandler(const winrt::Windows::Foundation::IInspectable& sender, const winrt::Windows::Foundation::IInspectable& args) const; void _KeyboardServiceWarningInfoDismissHandler(const winrt::Windows::Foundation::IInspectable& sender, const winrt::Windows::Foundation::IInspectable& args) const; diff --git a/src/cascadia/TerminalSettingsModel/ActionArgs.h b/src/cascadia/TerminalSettingsModel/ActionArgs.h index 1da433ca092..072c54f2cfa 100644 --- a/src/cascadia/TerminalSettingsModel/ActionArgs.h +++ b/src/cascadia/TerminalSettingsModel/ActionArgs.h @@ -247,6 +247,7 @@ namespace winrt::Microsoft::Terminal::Settings::Model::implementation ACTION_ARG(winrt::hstring, Profile, L""); ACTION_ARG(Windows::Foundation::IReference, SuppressApplicationTitle, nullptr); ACTION_ARG(winrt::hstring, ColorScheme); + ACTION_ARG(Windows::Foundation::IReference, Elevate, nullptr); static constexpr std::string_view CommandlineKey{ "commandline" }; static constexpr std::string_view StartingDirectoryKey{ "startingDirectory" }; @@ -256,6 +257,7 @@ namespace winrt::Microsoft::Terminal::Settings::Model::implementation static constexpr std::string_view ProfileKey{ "profile" }; static constexpr std::string_view SuppressApplicationTitleKey{ "suppressApplicationTitle" }; static constexpr std::string_view ColorSchemeKey{ "colorScheme" }; + static constexpr std::string_view ElevateKey{ "elevate" }; public: hstring GenerateName() const; @@ -273,7 +275,8 @@ namespace winrt::Microsoft::Terminal::Settings::Model::implementation otherAsUs->_ProfileIndex == _ProfileIndex && otherAsUs->_Profile == _Profile && otherAsUs->_SuppressApplicationTitle == _SuppressApplicationTitle && - otherAsUs->_ColorScheme == _ColorScheme; + otherAsUs->_ColorScheme == _ColorScheme && + otherAsUs->_Elevate == _Elevate; } return false; }; @@ -289,6 +292,7 @@ namespace winrt::Microsoft::Terminal::Settings::Model::implementation JsonUtils::GetValueForKey(json, TabColorKey, args->_TabColor); JsonUtils::GetValueForKey(json, SuppressApplicationTitleKey, args->_SuppressApplicationTitle); JsonUtils::GetValueForKey(json, ColorSchemeKey, args->_ColorScheme); + JsonUtils::GetValueForKey(json, ElevateKey, args->_Elevate); return *args; } static Json::Value ToJson(const Model::NewTerminalArgs& val) @@ -307,6 +311,7 @@ namespace winrt::Microsoft::Terminal::Settings::Model::implementation JsonUtils::SetValueForKey(json, TabColorKey, args->_TabColor); JsonUtils::SetValueForKey(json, SuppressApplicationTitleKey, args->_SuppressApplicationTitle); JsonUtils::SetValueForKey(json, ColorSchemeKey, args->_ColorScheme); + JsonUtils::SetValueForKey(json, ElevateKey, args->_Elevate); return json; } Model::NewTerminalArgs Copy() const @@ -320,6 +325,7 @@ namespace winrt::Microsoft::Terminal::Settings::Model::implementation copy->_Profile = _Profile; copy->_SuppressApplicationTitle = _SuppressApplicationTitle; copy->_ColorScheme = _ColorScheme; + copy->_Elevate = _Elevate; return *copy; } size_t Hash() const @@ -338,6 +344,7 @@ namespace winrt::Microsoft::Terminal::Settings::Model::implementation h.write(Profile()); h.write(SuppressApplicationTitle()); h.write(ColorScheme()); + h.write(Elevate()); } }; } diff --git a/src/cascadia/TerminalSettingsModel/ActionArgs.idl b/src/cascadia/TerminalSettingsModel/ActionArgs.idl index 576e3704f96..61cc5d2f85d 100644 --- a/src/cascadia/TerminalSettingsModel/ActionArgs.idl +++ b/src/cascadia/TerminalSettingsModel/ActionArgs.idl @@ -117,13 +117,20 @@ namespace Microsoft.Terminal.Settings.Model String TabTitle; Windows.Foundation.IReference TabColor; String Profile; // Either a GUID or a profile's name if the GUID isn't a match + + // We use IReference<> to treat some args as nullable where null means + // "use the inherited value". See ProfileIndex, + // SuppressApplicationTitle, Elevate. Strings that behave this way just + // use `null` as "use the inherited value". + // ProfileIndex can be null (for "use the default"), so this needs to be // a IReference, so it's nullable Windows.Foundation.IReference ProfileIndex { get; }; - Windows.Foundation.IReference SuppressApplicationTitle; - String ColorScheme; + // This needs to be an optional so that the default value (null) does + // not modify whatever the profile's value is (either true or false) + Windows.Foundation.IReference Elevate { get; }; Boolean Equals(NewTerminalArgs other); String GenerateName(); diff --git a/src/cascadia/TerminalSettingsModel/MTSMSettings.h b/src/cascadia/TerminalSettingsModel/MTSMSettings.h index 0d5faab1769..663fff126c4 100644 --- a/src/cascadia/TerminalSettingsModel/MTSMSettings.h +++ b/src/cascadia/TerminalSettingsModel/MTSMSettings.h @@ -75,7 +75,8 @@ Author(s): X(hstring, TabTitle, "tabTitle") \ X(Model::BellStyle, BellStyle, "bellStyle", BellStyle::Audible) \ X(bool, UseAtlasEngine, "experimental.useAtlasEngine", false) \ - X(Windows::Foundation::Collections::IVector, BellSound, "bellSound", nullptr) + X(Windows::Foundation::Collections::IVector, BellSound, "bellSound", nullptr) \ + X(bool, Elevate, "elevate", false) #define MTSM_FONT_SETTINGS(X) \ X(hstring, FontFace, "face", DEFAULT_FONT_FACE) \ diff --git a/src/cascadia/TerminalSettingsModel/Profile.idl b/src/cascadia/TerminalSettingsModel/Profile.idl index 41b9c9e018f..d54c6431a53 100644 --- a/src/cascadia/TerminalSettingsModel/Profile.idl +++ b/src/cascadia/TerminalSettingsModel/Profile.idl @@ -81,5 +81,7 @@ namespace Microsoft.Terminal.Settings.Model INHERITABLE_PROFILE_SETTING(BellStyle, BellStyle); INHERITABLE_PROFILE_SETTING(Boolean, UseAtlasEngine); INHERITABLE_PROFILE_SETTING(Windows.Foundation.Collections.IVector, BellSound); + + INHERITABLE_PROFILE_SETTING(Boolean, Elevate); } } diff --git a/src/cascadia/TerminalSettingsModel/TerminalSettings.cpp b/src/cascadia/TerminalSettingsModel/TerminalSettings.cpp index bf854ab2e56..491affc5e7f 100644 --- a/src/cascadia/TerminalSettingsModel/TerminalSettings.cpp +++ b/src/cascadia/TerminalSettingsModel/TerminalSettings.cpp @@ -155,6 +155,14 @@ namespace winrt::Microsoft::Terminal::Settings::Model::implementation defaultSettings.ApplyColorScheme(scheme); } } + // Elevate on NewTerminalArgs is an optional value, so the default + // value (null) doesn't override a profile's value. Note that + // elevate:false in an already elevated terminal does nothing - the + // profile will still be launched elevated. + if (newTerminalArgs.Elevate()) + { + defaultSettings.Elevate(newTerminalArgs.Elevate().Value()); + } } return settingsPair; @@ -255,6 +263,8 @@ namespace winrt::Microsoft::Terminal::Settings::Model::implementation const til::color colorRef{ profile.TabColor().Value() }; _TabColor = static_cast(colorRef); } + + _Elevate = profile.Elevate(); } // Method Description: diff --git a/src/cascadia/TerminalSettingsModel/TerminalSettings.h b/src/cascadia/TerminalSettingsModel/TerminalSettings.h index 2c97dd8be9f..23ebb390b93 100644 --- a/src/cascadia/TerminalSettingsModel/TerminalSettings.h +++ b/src/cascadia/TerminalSettingsModel/TerminalSettings.h @@ -152,6 +152,8 @@ namespace winrt::Microsoft::Terminal::Settings::Model::implementation INHERITABLE_SETTING(Model::TerminalSettings, hstring, PixelShaderPath); INHERITABLE_SETTING(Model::TerminalSettings, bool, IntenseIsBold); + INHERITABLE_SETTING(Model::TerminalSettings, bool, Elevate, false); + private: std::optional> _ColorTable; gsl::span _getColorTableImpl(); diff --git a/src/cascadia/TerminalSettingsModel/TerminalSettings.idl b/src/cascadia/TerminalSettingsModel/TerminalSettings.idl index d6b6e395d6d..e2345e0748e 100644 --- a/src/cascadia/TerminalSettingsModel/TerminalSettings.idl +++ b/src/cascadia/TerminalSettingsModel/TerminalSettings.idl @@ -39,5 +39,7 @@ namespace Microsoft.Terminal.Settings.Model String Commandline { set; }; String StartingDirectory { set; }; String EnvironmentVariables { set; }; + + Boolean Elevate; }; }