Merge commit 'refs/pull/11390/merge' of https://github.com/microsoft/terminal into dev/miniksa/selfhost-1.12

This commit is contained in:
Michael Niksa 2021-10-05 09:24:54 -07:00
commit d96d72dc03
11 changed files with 281 additions and 64 deletions

View file

@ -189,13 +189,12 @@ cacafire
callee
capslock
CARETBLINKINGENABLED
carlos
CARRIAGERETURN
cascadia
cassert
castsi
catid
carlos
zamora
cazamor
CBash
cbegin
@ -1232,6 +1231,7 @@ KLF
KLMNO
KLMNOPQRST
KLMNOPQRSTQQQQQ
KPRIORITY
KVM
langid
LANGUAGELIST
@ -1803,6 +1803,7 @@ POSX
POSXSCROLL
POSYSCROLL
ppci
PPEB
ppf
ppguid
ppidl
@ -2850,6 +2851,7 @@ YSize
YSubstantial
YVIRTUALSCREEN
YWalk
zamora
ZCmd
ZCtrl
zsh

View file

@ -218,7 +218,7 @@ namespace winrt::TerminalApp::implementation
_RegisterActionCallbacks();
// Hook up inbound connection event handler
TerminalConnection::ConptyConnection::NewConnection({ this, &TerminalPage::_OnNewConnection });
ConptyConnection::NewConnection({ this, &TerminalPage::_OnNewConnection });
//Event Bindings (Early)
_newTabButton.Click([weakThis{ get_weak() }](auto&&, auto&&) {
@ -2700,38 +2700,13 @@ namespace winrt::TerminalApp::implementation
return _isAlwaysOnTop;
}
HRESULT TerminalPage::_OnNewConnection(winrt::Microsoft::Terminal::TerminalConnection::ITerminalConnection connection)
HRESULT TerminalPage::_OnNewConnection(const ConptyConnection& connection)
{
// We need to be on the UI thread in order for _OpenNewTab to run successfully.
// HasThreadAccess will return true if we're currently on a UI thread and false otherwise.
// When we're on a COM thread, we'll need to dispatch the calls to the UI thread
// and wait on it hence the locking mechanism.
if (Dispatcher().HasThreadAccess())
{
try
{
NewTerminalArgs newTerminalArgs{};
// TODO GH#10952: When we pass the actual commandline (or originating application), the
// settings model can choose the right settings based on command matching, or synthesize
// a profile from the registry/link settings (TODO GH#9458).
// TODO GH#9458: Get and pass the LNK/EXE filenames.
// Passing in a commandline forces GetProfileForArgs to use Base Layer instead of Default Profile;
// in the future, it can make a better decision based on the value we pull out of the process handle.
// TODO GH#5047: When we hang on to the N.T.A., try not to spawn "default... .exe" :)
newTerminalArgs.Commandline(L"default-terminal-invocation-placeholder");
const auto profile{ _settings.GetProfileForArgs(newTerminalArgs) };
const auto settings{ TerminalSettings::CreateWithProfile(_settings, profile, *_bindings) };
_CreateNewTabWithProfileAndSettings(profile, settings, connection);
// Request a summon of this window to the foreground
_SummonWindowRequestedHandlers(*this, nullptr);
}
CATCH_RETURN();
return S_OK;
}
else
if (!Dispatcher().HasThreadAccess())
{
til::latch latch{ 1 };
HRESULT finalVal = S_OK;
@ -2739,13 +2714,27 @@ namespace winrt::TerminalApp::implementation
Dispatcher().RunAsync(CoreDispatcherPriority::Normal, [&]() {
// Re-running ourselves under the dispatcher will cause us to take the first branch above.
finalVal = _OnNewConnection(connection);
latch.count_down();
});
latch.wait();
return finalVal;
}
try
{
NewTerminalArgs newTerminalArgs;
newTerminalArgs.Commandline(connection.Commandline());
const auto profile{ _settings.GetProfileForArgs(newTerminalArgs) };
const auto settings{ TerminalSettings::CreateWithProfile(_settings, profile, *_bindings) };
_CreateNewTabWithProfileAndSettings(profile, settings, connection);
// Request a summon of this window to the foreground
_SummonWindowRequestedHandlers(*this, nullptr);
return S_OK;
}
CATCH_RETURN()
}
// Method Description:

View file

@ -388,7 +388,7 @@ namespace winrt::TerminalApp::implementation
winrt::Microsoft::Terminal::Settings::Model::Command _lastPreviewedCommand{ nullptr };
std::vector<std::function<void()>> _restorePreviewFuncs{};
HRESULT _OnNewConnection(winrt::Microsoft::Terminal::TerminalConnection::ITerminalConnection connection);
HRESULT _OnNewConnection(const winrt::Microsoft::Terminal::TerminalConnection::ConptyConnection& connection);
void _HandleToggleInboundPty(const IInspectable& sender, const Microsoft::Terminal::Settings::Model::ActionEventArgs& args);
void _WindowRenamerActionClick(const IInspectable& sender, const IInspectable& eventArgs);

View file

@ -1,12 +1,12 @@
// Copyright (c) Microsoft Corporation.
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT license.
#include "pch.h"
#include "ConptyConnection.h"
#include <windows.h>
#include <userenv.h>
#include <UserEnv.h>
#include <winternl.h>
#include "ConptyConnection.g.cpp"
#include "CTerminalHandoff.h"
@ -276,24 +276,17 @@ namespace winrt::Microsoft::Terminal::TerminalConnection::implementation
const HANDLE hClientProcess) :
_initialRows{ 25 },
_initialCols{ 80 },
_commandline{ L"" },
_startingDirectory{ L"" },
_startingTitle{ L"" },
_environment{ nullptr },
_guid{},
_u8State{},
_u16Str{},
_buffer{},
_inPipe{ hIn },
_outPipe{ hOut }
{
THROW_IF_FAILED(ConptyPackPseudoConsole(hServerProcess, hRef, hSig, &_hPC));
if (_guid == guid{})
{
_guid = Utils::CreateGuid();
}
_piClient.hProcess = hClientProcess;
try
{
_commandline = _commandlineFromProcess(hClientProcess);
}
CATCH_LOG()
}
// Function Description:
@ -355,6 +348,11 @@ namespace winrt::Microsoft::Terminal::TerminalConnection::implementation
return _guid;
}
winrt::hstring ConptyConnection::Commandline() const
{
return _commandline;
}
void ConptyConnection::Start()
try
{
@ -560,6 +558,41 @@ namespace winrt::Microsoft::Terminal::TerminalConnection::implementation
}
CATCH_LOG()
// Returns the command line of the given process.
// Requires PROCESS_BASIC_INFORMATION | PROCESS_VM_READ privileges.
winrt::hstring ConptyConnection::_commandlineFromProcess(HANDLE process)
{
// I know MSDN documents NtQueryInformationProcess with Reserved1/2/3
// fields, but... uh... that feels like security by obfuscation.
// .NET kindly published this struct before us.
struct PROCESS_BASIC_INFORMATION
{
NTSTATUS ExitStatus;
PPEB PebBaseAddress;
ULONG_PTR AffinityMask;
KPRIORITY BasePriority;
ULONG_PTR UniqueProcessId;
ULONG_PTR InheritedFromUniqueProcessId;
} info;
THROW_IF_NTSTATUS_FAILED(NtQueryInformationProcess(process, ProcessBasicInformation, &info, sizeof(info), nullptr));
// PEB: Process Environment Block
// This is a funny structure allocated by the kernel which contains all sorts of useful
// information, only a tiny fraction of which are documented publicly unfortunately.
// Fortunately however it contains a copy of the command line the process launched with.
PEB peb;
THROW_IF_WIN32_BOOL_FALSE(ReadProcessMemory(process, info.PebBaseAddress, &peb, sizeof(peb), nullptr));
RTL_USER_PROCESS_PARAMETERS params;
THROW_IF_WIN32_BOOL_FALSE(ReadProcessMemory(process, peb.ProcessParameters, &params, sizeof(params), nullptr));
// Yeah I know... Don't use "impl" stuff... But why do you make something _that_ useful private? :(
// The hstring_builder allows us to create a hstring without intermediate copies. Neat!
winrt::impl::hstring_builder commandline{ params.CommandLine.Length / 2u };
THROW_IF_WIN32_BOOL_FALSE(ReadProcessMemory(process, params.CommandLine.Buffer, commandline.data(), params.CommandLine.Length, nullptr));
return commandline.to_hstring();
}
DWORD ConptyConnection::_OutputThread()
{
// Keep us alive until the output thread terminates; the destructor
@ -636,8 +669,7 @@ namespace winrt::Microsoft::Terminal::TerminalConnection::implementation
HRESULT ConptyConnection::NewHandoff(HANDLE in, HANDLE out, HANDLE signal, HANDLE ref, HANDLE server, HANDLE client) noexcept
try
{
auto conn = winrt::make<implementation::ConptyConnection>(signal, in, out, ref, server, client);
_newConnectionHandlers(conn);
_newConnectionHandlers(winrt::make<ConptyConnection>(signal, in, out, ref, server, client));
return S_OK;
}

View file

@ -38,6 +38,7 @@ namespace winrt::Microsoft::Terminal::TerminalConnection::implementation
void ClearBuffer();
winrt::guid Guid() const noexcept;
winrt::hstring Commandline() const;
static void StartInboundListener();
static void StopInboundListener();
@ -56,12 +57,13 @@ namespace winrt::Microsoft::Terminal::TerminalConnection::implementation
WINRT_CALLBACK(TerminalOutput, TerminalOutputHandler);
private:
static HRESULT NewHandoff(HANDLE in, HANDLE out, HANDLE signal, HANDLE ref, HANDLE server, HANDLE client) noexcept;
static winrt::hstring _commandlineFromProcess(HANDLE process);
HRESULT _LaunchAttachedClient() noexcept;
void _indicateExitWithStatus(unsigned int status) noexcept;
void _ClientTerminated() noexcept;
static HRESULT NewHandoff(HANDLE in, HANDLE out, HANDLE signal, HANDLE ref, HANDLE server, HANDLE client) noexcept;
uint32_t _initialRows{};
uint32_t _initialCols{};
hstring _commandline{};

View file

@ -5,10 +5,13 @@ import "ITerminalConnection.idl";
namespace Microsoft.Terminal.TerminalConnection
{
delegate void NewConnectionHandler(ConptyConnection connection);
[default_interface] runtimeclass ConptyConnection : ITerminalConnection
{
ConptyConnection();
Guid Guid { get; };
String Commandline { get; };
void ClearBuffer();
static event NewConnectionHandler NewConnection;

View file

@ -29,6 +29,4 @@ namespace Microsoft.Terminal.TerminalConnection
event Windows.Foundation.TypedEventHandler<ITerminalConnection, Object> StateChanged;
ConnectionState State { get; };
};
delegate void NewConnectionHandler(ITerminalConnection connection);
}

View file

@ -10,6 +10,9 @@
#include <LibraryResources.h>
#include <VersionHelpers.h>
#include <shellapi.h>
#include <shlwapi.h>
using namespace winrt::Microsoft::Terminal;
using namespace winrt::Microsoft::Terminal::Settings;
using namespace winrt::Microsoft::Terminal::Settings::Model::implementation;
@ -529,9 +532,12 @@ Model::Profile CascadiaSettings::GetProfileForArgs(const Model::NewTerminalArgs&
{
if (newTerminalArgs)
{
if (auto profile = GetProfileByName(newTerminalArgs.Profile()))
if (const auto name = newTerminalArgs.Profile(); !name.empty())
{
return profile;
if (auto profile = GetProfileByName(name))
{
return profile;
}
}
if (const auto index = newTerminalArgs.ProfileIndex())
@ -541,6 +547,14 @@ Model::Profile CascadiaSettings::GetProfileForArgs(const Model::NewTerminalArgs&
return profile;
}
}
if (const auto commandLine = newTerminalArgs.Commandline(); !commandLine.empty())
{
if (auto profile = _getProfileForCommandLine(commandLine))
{
return profile;
}
}
}
if constexpr (Feature_ShowProfileDefaultsInSettings::IsEnabled())
@ -563,6 +577,179 @@ Model::Profile CascadiaSettings::GetProfileForArgs(const Model::NewTerminalArgs&
}
}
// The method does some crude command line matching for our console hand-off support.
// If you have hand-off enabled and start PowerShell from the start menu we might be called with
// "C:\Program Files\PowerShell\7\pwsh.exe -WorkingDirectory ~"
// This function then checks all known user profiles for one that's compatible with the commandLine.
// In this case we might have a profile with the command line
// "C:\Program Files\PowerShell\7\pwsh.exe"
// This function will then match this profile return it.
//
// If no matching profile could be found a nullptr will be returned.
Model::Profile CascadiaSettings::_getProfileForCommandLine(const winrt::hstring& commandLine) const
{
// We're going to cache all the command lines we got, as
// _normalizeCommandLine is a relatively heavy operation.
std::call_once(_commandLinesCacheOnce, [this]() {
_commandLinesCache.reserve(_allProfiles.Size());
for (const auto& profile : _allProfiles)
{
if (const auto cmd = profile.Commandline(); !cmd.empty())
{
_commandLinesCache.emplace_back(_normalizeCommandLine(cmd.c_str()), profile);
}
}
// We're trying to find the command line with the longest common prefix below.
// Given the commandLine "foo.exe -bar -baz" and these two user profiles:
// * "foo.exe"
// * "foo.exe -bar"
// we want to choose the second one. By sorting the _commandLinesCache in a descending order
// by command line length, we can return from this function the moment we found a matching
// profile as there cannot possibly be any other profile anymore with a longer command line.
std::stable_sort(_commandLinesCache.begin(), _commandLinesCache.end(), [](const auto& lhs, const auto& rhs) {
return lhs.first.size() > rhs.first.size();
});
});
const auto needle = _normalizeCommandLine(commandLine.c_str());
// til::starts_with(string, prefix) will always return false if prefix.size() > string.size().
// --> Using binary search we can safely skip all items in _commandLinesCache where .first.size() > needle.size().
const auto end = _commandLinesCache.end();
auto it = std::lower_bound(_commandLinesCache.begin(), end, needle, [&](const auto& lhs, const auto& rhs) {
return lhs.first.size() > rhs.size();
});
// `it` is now at a position where it->first.size() <= needle.size().
// Hopefully we'll now find a command line with matching prefix.
for (; it != end; ++it)
{
if (til::starts_with(needle, it->first))
{
return it->second;
}
}
return nullptr;
}
// Given a commandLine like the following:
// * "C:\WINDOWS\System32\cmd.exe"
// * "pwsh -WorkingDirectory ~"
// * "C:\Program Files\PowerShell\7\pwsh.exe"
// * "C:\Program Files\PowerShell\7\pwsh.exe -WorkingDirectory ~"
//
// This function returns:
// * "C:\Windows\System32\cmd.exe"
// * "C:\Program Files\PowerShell\7\pwsh.exe\0-WorkingDirectory\0~"
// * "C:\Program Files\PowerShell\7\pwsh.exe"
// * "C:\Program Files\PowerShell\7\pwsh.exe\0-WorkingDirectory\0~"
//
// The resulting strings are then used for comparisons in _getProfileForCommandLine().
// For instance a resulting string of
// "C:\Program Files\PowerShell\7\pwsh.exe"
// is considered a compatible profile with
// "C:\Program Files\PowerShell\7\pwsh.exe -WorkingDirectory ~"
// as it shares the same (normalized) prefix.
std::wstring CascadiaSettings::_normalizeCommandLine(LPCWSTR commandLine)
{
// Turn "%SystemRoot%\System32\cmd.exe" into "C:\WINDOWS\System32\cmd.exe".
// We do this early, as environment variables might occur anywhere in the commandLine.
std::wstring normalized;
THROW_IF_FAILED(wil::ExpandEnvironmentStringsW(commandLine, normalized));
// One of the most important things this function does is to strip quotes.
// That way the commandLine "foo.exe -bar" and "\"foo.exe\" \"-bar\"" appear identical.
// We'll abuse CommandLineToArgvW for that as it's close to what CreateProcessW uses.
int argc = 0;
const auto argv = CommandLineToArgvW(normalized.c_str(), &argc);
THROW_LAST_ERROR_IF(!argc);
const auto argvRelease = wil::scope_exit([=]() { LocalFree(argv); });
// The given commandLine should start with an executable name or path.
// For instance given the following argv arrays:
// * {"C:\WINDOWS\System32\cmd.exe"}
// * {"pwsh", "-WorkingDirectory", "~"}
// * {"C:\Program", "Files\PowerShell\7\pwsh.exe"}
// ^^^^
// Notice how there used to be a space in the path, which was split by ExpandEnvironmentStringsW().
// CreateProcessW() supports such atrocities, so we got to do the same.
// * {"C:\Program Files\PowerShell\7\pwsh.exe", "-WorkingDirectory", "~"}
//
// This loop tries to resolve relative paths, as well as executable names in %PATH%
// into absolute paths and normalizes them. The results for the above would be:
// * "C:\Windows\System32\cmd.exe"
// * "C:\Program Files\PowerShell\7\pwsh.exe"
// * "C:\Program Files\PowerShell\7\pwsh.exe"
// * "C:\Program Files\PowerShell\7\pwsh.exe"
for (;;)
{
// CreateProcessW uses RtlGetExePath to get the lpPath for SearchPathW.
// The difference between the behavior of SearchPathW if lpPath is nullptr and what RtlGetExePath returns
// seems to be mostly whether SafeProcessSearchMode is respected and the support for relative paths.
// Windows Terminal makes the use relative paths rather impractical which is why we simply dropped the call to RtlGetExePath.
const auto status = wil::SearchPathW(nullptr, argv[0], L".exe", normalized);
if (status == S_OK)
{
std::filesystem::path path{ std::move(normalized) };
// ExpandEnvironmentStringsW() might have returned a string that's not in the canonical capitalization.
// For instance %SystemRoot% is set to C:\WINDOWS on my system (ugh), even though the path is actually C:\Windows.
// We need to fix this as case-sensitive path comparisons will fail otherwise (Windows supports case-sensitive file systems).
// If we fail to resolve the path for whatever reason (pretty unlikely given that SearchPathW found it)
// we fall back to leaving the path as is. Better than throwing a random exception and making this unusable.
{
std::error_code ec;
auto canonicalPath = canonical(path, ec);
if (!ec)
{
path = std::move(canonicalPath);
}
}
// std::filesystem::path has no way to extract the internal path.
// So about that.... I own you, computer. Give me that path.
normalized = std::move(const_cast<std::wstring&>(path.native()));
break;
}
// If the file path couldn't be found by SearchPathW this could be the result of us being given a commandLine
// like "C:\foo bar\baz.exe -arg" which is resolved to the argv array {"C:\foo", "bar\baz.exe", "-arg"}.
// Just like CreateProcessW() we thus try to concatenate arguments until we successfully resolve a valid path.
// Of course we can only do that if we have at least 2 remaining arguments in argv.
// All other error types aren't handled at the moment.
if (argc < 2 || status != HRESULT_FROM_WIN32(ERROR_FILE_NOT_FOUND))
{
break;
}
// As described in the comment right above, we concatenate arguments in an attempt to resolve a valid path.
// The code below turns argv from {"C:\foo", "bar\baz.exe", "-arg"} into {"C:\foo bar\baz.exe", "-arg"}.
// The code abuses the fact that CommandLineToArgvW allocates all arguments back-to-back on the heap separated by '\0'.
argv[1][-1] = L' ';
--argc;
}
// We've (hopefully) finished resolving the path to the executable.
// We're now going to append all remaining arguments to the resulting string.
// If argv is {"C:\Program Files\PowerShell\7\pwsh.exe", "-WorkingDirectory", "~"},
// then we'll get "C:\Program Files\PowerShell\7\pwsh.exe\0-WorkingDirectory\0~"
if (argc > 1)
{
// normalized contains a canonical form of argv[0] at this point.
// -1 allows us to include the \0 between argv[0] and argv[1] in the call to append().
const auto beg = argv[1] - 1;
const auto lastArg = argv[argc - 1];
const auto end = lastArg + wcslen(lastArg);
normalized.append(beg, end);
}
return normalized;
}
// Method Description:
// - Helper to get a profile given a name that could be a guid or an actual name.
// Arguments:

View file

@ -140,8 +140,10 @@ namespace winrt::Microsoft::Terminal::Settings::Model::implementation
private:
static const std::filesystem::path& _settingsPath();
static std::wstring _normalizeCommandLine(LPCWSTR commandLine);
winrt::com_ptr<implementation::Profile> _createNewProfile(const std::wstring_view& name) const;
Model::Profile _getProfileForCommandLine(const winrt::hstring& commandLine) const;
void _resolveDefaultProfile() const;
@ -165,6 +167,10 @@ namespace winrt::Microsoft::Terminal::Settings::Model::implementation
// defterm
Model::DefaultTerminal _currentDefaultTerminal{ nullptr };
// GetProfileForArgs cache
mutable std::once_flag _commandLinesCacheOnce;
mutable std::vector<std::pair<std::wstring, Model::Profile>> _commandLinesCache;
};
}

View file

@ -265,14 +265,12 @@ static bool getWslNames(const wil::unique_hkey& wslRootKey,
std::wstring buffer;
auto result = wil::AdaptFixedSizeToAllocatedResult<std::wstring, 256>(buffer, [&](PWSTR value, size_t valueLength, size_t* valueLengthNeededWithNull) -> HRESULT {
auto length = static_cast<DWORD>(valueLength);
auto length = gsl::narrow<DWORD>(valueLength * sizeof(wchar_t));
const auto status = RegQueryValueExW(distroKey.get(), RegKeyDistroName, 0, nullptr, reinterpret_cast<BYTE*>(value), &length);
// length will receive the number of bytes - convert to a number of
// wchar_t's. AdaptFixedSizeToAllocatedResult will resize buffer to
// valueLengthNeededWithNull
*valueLengthNeededWithNull = (length / sizeof(wchar_t));
// If you add one for another trailing null, then there'll actually
// be _two_ trailing nulls in the buffer.
// length will receive the number of bytes including trailing null byte. Convert to a number of wchar_t's.
// AdaptFixedSizeToAllocatedResult will then resize buffer to valueLengthNeededWithNull.
// We're rounding up to prevent infinite loops if the data isn't a REG_SZ and length isn't divisible by 2.
*valueLengthNeededWithNull = (length + sizeof(wchar_t) - 1) / sizeof(wchar_t);
return status == ERROR_MORE_DATA ? S_OK : HRESULT_FROM_WIN32(status);
});

View file

@ -453,7 +453,7 @@ try
RETURN_IF_WIN32_BOOL_FALSE(CreatePipe(outPipeTheirSide.addressof(), outPipeOurSide.addressof(), nullptr, 0));
wil::unique_handle clientProcess{ OpenProcess(PROCESS_QUERY_INFORMATION | SYNCHRONIZE, TRUE, static_cast<DWORD>(connectMessage->Descriptor.Process)) };
wil::unique_handle clientProcess{ OpenProcess(PROCESS_QUERY_INFORMATION | PROCESS_VM_READ | SYNCHRONIZE, TRUE, static_cast<DWORD>(connectMessage->Descriptor.Process)) };
RETURN_LAST_ERROR_IF_NULL(clientProcess.get());
wil::unique_handle refHandle;