2019-05-03 00:29:04 +02:00
|
|
|
|
// Copyright (c) Microsoft Corporation.
|
|
|
|
|
// Licensed under the MIT license.
|
|
|
|
|
|
|
|
|
|
#include "pch.h"
|
2019-12-09 20:07:08 +01:00
|
|
|
|
|
|
|
|
|
// We have to define GSL here, not PCH
|
|
|
|
|
// because TelnetConnection has a conflicting GSL implementation.
|
|
|
|
|
#include <gsl/gsl>
|
|
|
|
|
|
2019-05-03 00:29:04 +02:00
|
|
|
|
#include "ConptyConnection.h"
|
|
|
|
|
|
2019-11-07 00:09:01 +01:00
|
|
|
|
#include <windows.h>
|
|
|
|
|
|
|
|
|
|
#include "ConptyConnection.g.cpp"
|
|
|
|
|
|
|
|
|
|
#include "../../types/inc/Utils.hpp"
|
|
|
|
|
#include "../../types/inc/Environment.hpp"
|
2019-11-25 23:22:29 +01:00
|
|
|
|
#include "LibraryResources.h"
|
2019-11-07 00:09:01 +01:00
|
|
|
|
|
|
|
|
|
using namespace ::Microsoft::Console;
|
2019-05-03 00:29:04 +02:00
|
|
|
|
|
2019-11-25 23:22:29 +01:00
|
|
|
|
// Notes:
|
|
|
|
|
// There is a number of ways that the Conpty connection can be terminated (voluntarily or not):
|
|
|
|
|
// 1. The connection is Close()d
|
|
|
|
|
// 2. The pseudoconsole or process cannot be spawned during Start()
|
|
|
|
|
// 3. The client process exits with a code.
|
|
|
|
|
// (Successful (0) or any other code)
|
|
|
|
|
// 4. The read handle is terminated.
|
|
|
|
|
// (This usually happens when the pseudoconsole host crashes.)
|
|
|
|
|
// In each of these termination scenarios, we need to be mindful of tripping the others.
|
|
|
|
|
// Closing the pseudoconsole in response to the client exiting (3) can trigger (4).
|
|
|
|
|
// Close() (1) will cause the automatic triggering of (3) and (4).
|
|
|
|
|
// In a lot of cases, we use the connection state to stop "flapping."
|
|
|
|
|
//
|
|
|
|
|
// To figure out where we handle these, search for comments containing "EXIT POINT"
|
|
|
|
|
|
2019-05-03 00:29:04 +02:00
|
|
|
|
namespace winrt::Microsoft::Terminal::TerminalConnection::implementation
|
|
|
|
|
{
|
2019-11-07 00:09:01 +01:00
|
|
|
|
// Function Description:
|
|
|
|
|
// - creates some basic anonymous pipes and passes them to CreatePseudoConsole
|
|
|
|
|
// Arguments:
|
|
|
|
|
// - size: The size of the conpty to create, in characters.
|
|
|
|
|
// - phInput: Receives the handle to the newly-created anonymous pipe for writing input to the conpty.
|
|
|
|
|
// - phOutput: Receives the handle to the newly-created anonymous pipe for reading the output of the conpty.
|
|
|
|
|
// - phPc: Receives a token value to identify this conpty
|
2020-01-03 19:44:27 +01:00
|
|
|
|
#pragma warning(suppress : 26430) // This statement sufficiently checks the out parameters. Analyzer cannot find this.
|
|
|
|
|
static HRESULT _CreatePseudoConsoleAndPipes(const COORD size, const DWORD dwFlags, HANDLE* phInput, HANDLE* phOutput, HPCON* phPC) noexcept
|
2019-11-07 00:09:01 +01:00
|
|
|
|
{
|
|
|
|
|
RETURN_HR_IF(E_INVALIDARG, phPC == nullptr || phInput == nullptr || phOutput == nullptr);
|
|
|
|
|
|
|
|
|
|
wil::unique_hfile outPipeOurSide, outPipePseudoConsoleSide;
|
|
|
|
|
wil::unique_hfile inPipeOurSide, inPipePseudoConsoleSide;
|
|
|
|
|
|
2020-01-03 19:44:27 +01:00
|
|
|
|
RETURN_IF_WIN32_BOOL_FALSE(CreatePipe(&inPipePseudoConsoleSide, &inPipeOurSide, nullptr, 0));
|
|
|
|
|
RETURN_IF_WIN32_BOOL_FALSE(CreatePipe(&outPipeOurSide, &outPipePseudoConsoleSide, nullptr, 0));
|
2019-11-16 02:02:38 +01:00
|
|
|
|
RETURN_IF_FAILED(ConptyCreatePseudoConsole(size, inPipePseudoConsoleSide.get(), outPipePseudoConsoleSide.get(), dwFlags, phPC));
|
2019-11-07 00:09:01 +01:00
|
|
|
|
*phInput = inPipeOurSide.release();
|
|
|
|
|
*phOutput = outPipeOurSide.release();
|
|
|
|
|
return S_OK;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Function Description:
|
|
|
|
|
// - launches the client application attached to the new pseudoconsole
|
|
|
|
|
HRESULT ConptyConnection::_LaunchAttachedClient() noexcept
|
2020-01-03 19:44:27 +01:00
|
|
|
|
try
|
2019-11-07 00:09:01 +01:00
|
|
|
|
{
|
|
|
|
|
STARTUPINFOEX siEx{ 0 };
|
|
|
|
|
siEx.StartupInfo.cb = sizeof(STARTUPINFOEX);
|
|
|
|
|
siEx.StartupInfo.dwFlags = STARTF_USESTDHANDLES;
|
2020-01-03 19:44:27 +01:00
|
|
|
|
SIZE_T size{};
|
2019-11-07 00:09:01 +01:00
|
|
|
|
// This call will return an error (by design); we are ignoring it.
|
2020-01-03 19:44:27 +01:00
|
|
|
|
InitializeProcThreadAttributeList(nullptr, 1, 0, &size);
|
|
|
|
|
#pragma warning(suppress : 26414) // We don't move/touch this smart pointer, but we have to allocate strangely for the adjustable size list.
|
2019-11-07 00:09:01 +01:00
|
|
|
|
auto attrList{ std::make_unique<std::byte[]>(size) };
|
2020-01-03 19:44:27 +01:00
|
|
|
|
#pragma warning(suppress : 26490) // We have to use reinterpret_cast because we allocated a byte array as a proxy for the adjustable size list.
|
2019-11-07 00:09:01 +01:00
|
|
|
|
siEx.lpAttributeList = reinterpret_cast<PPROC_THREAD_ATTRIBUTE_LIST>(attrList.get());
|
2020-01-03 19:44:27 +01:00
|
|
|
|
RETURN_IF_WIN32_BOOL_FALSE(InitializeProcThreadAttributeList(siEx.lpAttributeList, 1, 0, &size));
|
2019-11-07 00:09:01 +01:00
|
|
|
|
|
|
|
|
|
RETURN_IF_WIN32_BOOL_FALSE(UpdateProcThreadAttribute(siEx.lpAttributeList,
|
|
|
|
|
0,
|
|
|
|
|
PROC_THREAD_ATTRIBUTE_PSEUDOCONSOLE,
|
|
|
|
|
_hPC.get(),
|
|
|
|
|
sizeof(HPCON),
|
2020-01-03 19:44:27 +01:00
|
|
|
|
nullptr,
|
|
|
|
|
nullptr));
|
2019-11-07 00:09:01 +01:00
|
|
|
|
|
2019-11-13 01:47:57 +01:00
|
|
|
|
std::wstring cmdline{ wil::ExpandEnvironmentStringsW<std::wstring>(_commandline.c_str()) }; // mutable copy -- required for CreateProcessW
|
2019-11-07 00:09:01 +01:00
|
|
|
|
|
|
|
|
|
Utils::EnvironmentVariableMapW environment;
|
2020-01-09 20:23:48 +01:00
|
|
|
|
auto zeroEnvMap = wil::scope_exit([&] {
|
|
|
|
|
// Can't zero the keys, but at least we can zero the values.
|
|
|
|
|
for (auto& [name, value] : environment)
|
|
|
|
|
{
|
|
|
|
|
::SecureZeroMemory(value.data(), value.size() * sizeof(decltype(value.begin())::value_type));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
environment.clear();
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// Populate the environment map with the current environment.
|
|
|
|
|
RETURN_IF_FAILED(Utils::UpdateEnvironmentMapW(environment));
|
2019-11-07 00:09:01 +01:00
|
|
|
|
|
|
|
|
|
{
|
|
|
|
|
// Convert connection Guid to string and ignore the enclosing '{}'.
|
|
|
|
|
std::wstring wsGuid{ Utils::GuidToString(_guid) };
|
|
|
|
|
wsGuid.pop_back();
|
|
|
|
|
|
2020-01-03 19:44:27 +01:00
|
|
|
|
const auto guidSubStr = std::wstring_view{ wsGuid }.substr(1);
|
2019-11-07 00:09:01 +01:00
|
|
|
|
|
|
|
|
|
// Ensure every connection has the unique identifier in the environment.
|
2020-01-09 20:23:48 +01:00
|
|
|
|
environment.insert_or_assign(L"WT_SESSION", guidSubStr.data());
|
|
|
|
|
|
|
|
|
|
auto wslEnv = environment[L"WSLENV"]; // We always want to load something, even if it's blank.
|
|
|
|
|
// WSLENV is a colon-delimited list of environment variables (+flags) that should appear inside WSL
|
|
|
|
|
// https://devblogs.microsoft.com/commandline/share-environment-vars-between-wsl-and-windows/
|
|
|
|
|
wslEnv = L"WT_SESSION:" + wslEnv; // prepend WT_SESSION to make sure it's visible inside WSL.
|
|
|
|
|
environment.insert_or_assign(L"WSLENV", wslEnv);
|
2019-11-07 00:09:01 +01:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
std::vector<wchar_t> newEnvVars;
|
2020-01-03 19:44:27 +01:00
|
|
|
|
auto zeroNewEnv = wil::scope_exit([&]() noexcept {
|
2019-11-07 00:09:01 +01:00
|
|
|
|
::SecureZeroMemory(newEnvVars.data(),
|
|
|
|
|
newEnvVars.size() * sizeof(decltype(newEnvVars.begin())::value_type));
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
RETURN_IF_FAILED(Utils::EnvironmentMapToEnvironmentStringsW(environment, newEnvVars));
|
|
|
|
|
|
|
|
|
|
LPWCH lpEnvironment = newEnvVars.empty() ? nullptr : newEnvVars.data();
|
|
|
|
|
|
|
|
|
|
// If we have a startingTitle, create a mutable character buffer to add
|
|
|
|
|
// it to the STARTUPINFO.
|
|
|
|
|
std::wstring mutableTitle{};
|
|
|
|
|
if (!_startingTitle.empty())
|
|
|
|
|
{
|
|
|
|
|
mutableTitle = _startingTitle;
|
|
|
|
|
siEx.StartupInfo.lpTitle = mutableTitle.data();
|
|
|
|
|
}
|
|
|
|
|
|
2019-11-11 19:44:49 +01:00
|
|
|
|
const wchar_t* const startingDirectory = _startingDirectory.size() > 0 ? _startingDirectory.c_str() : nullptr;
|
|
|
|
|
|
2019-11-07 00:09:01 +01:00
|
|
|
|
RETURN_IF_WIN32_BOOL_FALSE(CreateProcessW(
|
|
|
|
|
nullptr,
|
|
|
|
|
cmdline.data(),
|
|
|
|
|
nullptr, // lpProcessAttributes
|
|
|
|
|
nullptr, // lpThreadAttributes
|
|
|
|
|
false, // bInheritHandles
|
|
|
|
|
EXTENDED_STARTUPINFO_PRESENT | CREATE_UNICODE_ENVIRONMENT, // dwCreationFlags
|
|
|
|
|
lpEnvironment, // lpEnvironment
|
2019-11-11 19:44:49 +01:00
|
|
|
|
startingDirectory,
|
2019-11-07 00:09:01 +01:00
|
|
|
|
&siEx.StartupInfo, // lpStartupInfo
|
|
|
|
|
&_piClient // lpProcessInformation
|
|
|
|
|
));
|
|
|
|
|
|
|
|
|
|
DeleteProcThreadAttributeList(siEx.lpAttributeList);
|
|
|
|
|
|
2020-02-12 21:02:48 +01:00
|
|
|
|
const std::filesystem::path processName = wil::GetModuleFileNameExW<std::wstring>(_piClient.hProcess, nullptr);
|
|
|
|
|
_clientName = processName.filename().wstring();
|
|
|
|
|
|
|
|
|
|
#pragma warning(suppress : 26477 26485 26494 26482 26446) // We don't control TraceLoggingWrite
|
|
|
|
|
TraceLoggingWrite(
|
|
|
|
|
g_hTerminalConnectionProvider,
|
|
|
|
|
"ConPtyConnected",
|
|
|
|
|
TraceLoggingDescription("Event emitted when ConPTY connection is started"),
|
|
|
|
|
TraceLoggingGuid(_guid, "SessionGuid", "The WT_SESSION's GUID"),
|
|
|
|
|
TraceLoggingWideString(_clientName.c_str(), "Client", "The attached client process"),
|
|
|
|
|
TraceLoggingKeyword(MICROSOFT_KEYWORD_MEASURES),
|
|
|
|
|
TelemetryPrivacyDataTag(PDT_ProductAndServicePerformance));
|
|
|
|
|
|
2019-11-07 00:09:01 +01:00
|
|
|
|
return S_OK;
|
|
|
|
|
}
|
2020-01-03 19:44:27 +01:00
|
|
|
|
CATCH_RETURN();
|
2019-11-07 00:09:01 +01:00
|
|
|
|
|
|
|
|
|
ConptyConnection::ConptyConnection(const hstring& commandline,
|
|
|
|
|
const hstring& startingDirectory,
|
|
|
|
|
const hstring& startingTitle,
|
|
|
|
|
const uint32_t initialRows,
|
|
|
|
|
const uint32_t initialCols,
|
|
|
|
|
const guid& initialGuid) :
|
|
|
|
|
_initialRows{ initialRows },
|
|
|
|
|
_initialCols{ initialCols },
|
|
|
|
|
_commandline{ commandline },
|
|
|
|
|
_startingDirectory{ startingDirectory },
|
|
|
|
|
_startingTitle{ startingTitle },
|
2020-01-30 01:55:48 +01:00
|
|
|
|
_guid{ initialGuid },
|
|
|
|
|
_u8State{},
|
|
|
|
|
_u16Str{},
|
|
|
|
|
_buffer{}
|
2019-11-07 00:09:01 +01:00
|
|
|
|
{
|
|
|
|
|
if (_guid == guid{})
|
|
|
|
|
{
|
|
|
|
|
_guid = Utils::CreateGuid();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
winrt::guid ConptyConnection::Guid() const noexcept
|
2019-05-03 00:29:04 +02:00
|
|
|
|
{
|
2019-11-07 00:09:01 +01:00
|
|
|
|
return _guid;
|
2019-05-03 00:29:04 +02:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void ConptyConnection::Start()
|
2019-11-07 00:09:01 +01:00
|
|
|
|
try
|
2019-05-03 00:29:04 +02:00
|
|
|
|
{
|
2019-11-07 00:09:01 +01:00
|
|
|
|
const COORD dimensions{ gsl::narrow_cast<SHORT>(_initialCols), gsl::narrow_cast<SHORT>(_initialRows) };
|
Add support for "reflow"ing the Terminal buffer (#4741)
This PR adds support for "Resize with Reflow" to the Terminal. In
conhost, `ResizeWithReflow` is the function that's responsible for
reflowing wrapped lines of text as the buffer gets resized. Now that
#4415 has merged, we can also implement this in the Terminal. Now, when
the Terminal is resized, it will reflow the lines of it's buffer in the
same way that conhost does. This means, the terminal will no longer chop
off the ends of lines as the buffer is too small to represent them.
As a happy side effect of this PR, it also fixed #3490. This was a bug
that plagued me during the investigation into this functionality. The
original #3490 PR, #4354, tried to fix this bug with some heavy conpty
changes. Turns out, that only made things worse, and far more
complicated. When I really got to thinking about it, I realized "conhost
can handle this right, why can't the Terminal?". Turns out, by adding
resize with reflow, I was also able to fix this at the same time.
Conhost does a little bit of math after reflowing to attempt to keep the
viewport in the same relative place after a reflow. By re-using that
logic in the Terminal, I was able to fix #3490.
I also included that big ole test from #3490, because everyone likes
adding 60 test cases in a PR.
## References
* #4200 - this scenario
* #405/#4415 - conpty emits wrapped lines, which was needed for this PR
* #4403 - delayed EOL wrapping via conpty, which was also needed for
this
* #4354 - we don't speak of this PR anymore
## PR Checklist
* [x] Closes #1465
* [x] Closes #3490
* [x] Closes #4771
* [x] Tests added/passed
## EDIT: Changes to this PR on 5 March 2020
I learned more since my original version of this PR. I wrote that in
January, and despite my notes that say it was totally working, it
_really_ wasn't.
Part of the hard problem, as mentioned in #3490, is that the Terminal
might request a resize to (W, H-1), and while conpty is preparing that
frame, or before the terminal has received that frame, the Terminal
resizes to (W, H-2). Now, there aren't enough lines in the terminal
buffer to catch all the lines that conpty is about to emit. When that
happens, lines get duplicated in the buffer. From a UX perspective, this
certainly looks a lot worse than a couple lost lines. It looks like
utter chaos.
So I've introduced a new mode to conpty to try and counteract this
behavior. This behavior I'm calling "quirky resize". The **TL;DR** of
quirky resize mode is that conpty won't emit the entire buffer on a
resize, and will trust that the terminal is prepared to reflow it's
buffer on it's own.
This will enable the quirky resize behavior for applications that are
prepared for it. The "quirky resize" is "don't `InvalidateAll` when the
terminal resizes". This is added as a quirk as to not regress other
terminal applications that aren't prepared for this behavior
(gnome-terminal, conhost in particular). For those kinds of terminals,
when the buffer is resized, it's just going to lose lines. That's what
currently happens for them.
When the quirk is enabled, conpty won't repaint the entire buffer. This
gets around the "duplicated lines" issue that requesting multiple
resizes in a row can cause. However, for these terminals that are
unprepared, the conpty cursor might end up in the wrong position after a
quirky resize.
The case in point is maximizing the terminal. For maximizing
(height->50) from a buffer that's 30 lines tall, with the cursor on
y=30, this is what happens:
* With the quirk disabled, conpty reprints the entire buffer. This is
60 lines that get printed. This ends up blowing away about 20 lines
of scrollback history, as the terminal app would have tried to keep
the text pinned to the bottom of the window. The term. app moved the
viewport up 20 lines, and then the 50 lines of conpty output (30
lines of text, and 20 blank lines at the bottom) overwrote the lines
from the scrollback. This is bad, but not immediately obvious, and
is **what currently happens**.
* With the quirk enabled, conpty doesn't emit any lines, but the
actual content of the window is still only in the top 30 lines.
However, the terminal app has still moved 20 lines down from the
scrollback back into the viewport. So the terminal's cursor is at
y=50 now, but conpty's is at 30. This means that the terminal and
conpty are out of sync, and there's not a good way of re-syncing
these. It's very possible (trivial in `powershell`) that the new
output will jump up to y=30 override the existing output in the
terminal buffer.
The Windows Terminal is already prepared for this quirky behavior, so it
doesn't keep the output at the bottom of the window. It shifts it's
viewport down to match what conpty things the buffer looks like.
What happens when we have passthrough mode and WT is like "I would like
quirky resize"? I guess things will just work fine, cause there won't be
a buffer behind the passthrough app that the terminal cares about. Sure,
in the passthrough case the Terminal could _not_ quirky resize, but the
quirky resize won't be wrong.
2020-03-13 01:43:37 +01:00
|
|
|
|
THROW_IF_FAILED(_CreatePseudoConsoleAndPipes(dimensions, PSEUDOCONSOLE_RESIZE_QUIRK, &_inPipe, &_outPipe, &_hPC));
|
2019-11-07 00:09:01 +01:00
|
|
|
|
THROW_IF_FAILED(_LaunchAttachedClient());
|
2019-05-03 00:29:04 +02:00
|
|
|
|
|
2019-11-07 00:09:01 +01:00
|
|
|
|
_startTime = std::chrono::high_resolution_clock::now();
|
2019-05-03 00:29:04 +02:00
|
|
|
|
|
|
|
|
|
// Create our own output handling thread
|
2019-11-07 00:09:01 +01:00
|
|
|
|
// This must be done after the pipes are populated.
|
|
|
|
|
// Each connection needs to make sure to drain the output from its backing host.
|
|
|
|
|
_hOutputThread.reset(CreateThread(
|
|
|
|
|
nullptr,
|
|
|
|
|
0,
|
2020-01-03 19:44:27 +01:00
|
|
|
|
[](LPVOID lpParameter) noexcept {
|
|
|
|
|
ConptyConnection* const pInstance = static_cast<ConptyConnection*>(lpParameter);
|
|
|
|
|
if (pInstance)
|
|
|
|
|
{
|
|
|
|
|
return pInstance->_OutputThread();
|
|
|
|
|
}
|
|
|
|
|
return gsl::narrow_cast<DWORD>(E_INVALIDARG);
|
2019-11-07 00:09:01 +01:00
|
|
|
|
},
|
|
|
|
|
this,
|
|
|
|
|
0,
|
|
|
|
|
nullptr));
|
|
|
|
|
|
|
|
|
|
THROW_LAST_ERROR_IF_NULL(_hOutputThread);
|
|
|
|
|
|
|
|
|
|
_clientExitWait.reset(CreateThreadpoolWait(
|
2020-01-03 19:44:27 +01:00
|
|
|
|
[](PTP_CALLBACK_INSTANCE /*callbackInstance*/, PVOID context, PTP_WAIT /*wait*/, TP_WAIT_RESULT /*waitResult*/) noexcept {
|
|
|
|
|
ConptyConnection* const pInstance = static_cast<ConptyConnection*>(context);
|
|
|
|
|
if (pInstance)
|
|
|
|
|
{
|
|
|
|
|
pInstance->_ClientTerminated();
|
|
|
|
|
}
|
2019-11-07 00:09:01 +01:00
|
|
|
|
},
|
|
|
|
|
this,
|
|
|
|
|
nullptr));
|
|
|
|
|
|
|
|
|
|
SetThreadpoolWait(_clientExitWait.get(), _piClient.hProcess, nullptr);
|
2019-05-03 00:29:04 +02:00
|
|
|
|
|
2019-11-25 23:22:29 +01:00
|
|
|
|
_transitionToState(ConnectionState::Connected);
|
2019-05-03 00:29:04 +02:00
|
|
|
|
}
|
2019-11-07 00:09:01 +01:00
|
|
|
|
catch (...)
|
2019-05-03 00:29:04 +02:00
|
|
|
|
{
|
2019-11-25 23:22:29 +01:00
|
|
|
|
// EXIT POINT
|
2019-11-07 00:09:01 +01:00
|
|
|
|
const auto hr = wil::ResultFromCaughtException();
|
2019-05-03 00:29:04 +02:00
|
|
|
|
|
2020-01-03 19:44:27 +01:00
|
|
|
|
winrt::hstring failureText{ wil::str_printf<std::wstring>(RS_(L"ProcessFailedToLaunch").c_str(), gsl::narrow_cast<unsigned int>(hr), _commandline.c_str()) };
|
2019-11-25 23:22:29 +01:00
|
|
|
|
_TerminalOutputHandlers(failureText);
|
|
|
|
|
_transitionToState(ConnectionState::Failed);
|
|
|
|
|
|
|
|
|
|
// Tear down any state we may have accumulated.
|
|
|
|
|
_hPC.reset();
|
2019-05-03 00:29:04 +02:00
|
|
|
|
}
|
|
|
|
|
|
2019-11-25 23:22:29 +01:00
|
|
|
|
// Method Description:
|
|
|
|
|
// - prints out the "process exited" message formatted with the exit code
|
|
|
|
|
// Arguments:
|
|
|
|
|
// - status: the exit code.
|
|
|
|
|
void ConptyConnection::_indicateExitWithStatus(unsigned int status) noexcept
|
|
|
|
|
{
|
|
|
|
|
try
|
|
|
|
|
{
|
2020-01-03 19:44:27 +01:00
|
|
|
|
winrt::hstring exitText{ wil::str_printf<std::wstring>(RS_(L"ProcessExited").c_str(), status) };
|
2019-11-25 23:22:29 +01:00
|
|
|
|
_TerminalOutputHandlers(L"\r\n");
|
|
|
|
|
_TerminalOutputHandlers(exitText);
|
|
|
|
|
}
|
|
|
|
|
CATCH_LOG();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Method Description:
|
|
|
|
|
// - called when the client application (not necessarily its pty) exits for any reason
|
2019-11-07 00:09:01 +01:00
|
|
|
|
void ConptyConnection::_ClientTerminated() noexcept
|
2019-05-03 00:29:04 +02:00
|
|
|
|
{
|
2019-11-25 23:22:29 +01:00
|
|
|
|
if (_isStateAtOrBeyond(ConnectionState::Closing))
|
2019-11-07 00:09:01 +01:00
|
|
|
|
{
|
2019-11-25 23:22:29 +01:00
|
|
|
|
// This termination was expected.
|
2019-11-07 00:09:01 +01:00
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
2019-11-25 23:22:29 +01:00
|
|
|
|
// EXIT POINT
|
|
|
|
|
DWORD exitCode{ 0 };
|
|
|
|
|
GetExitCodeProcess(_piClient.hProcess, &exitCode);
|
|
|
|
|
|
|
|
|
|
// Signal the closing or failure of the process.
|
|
|
|
|
// Load bearing. Terminating the pseudoconsole will make the output thread exit unexpectedly,
|
|
|
|
|
// so we need to signal entry into the correct closing state before we do that.
|
|
|
|
|
_transitionToState(exitCode == 0 ? ConnectionState::Closed : ConnectionState::Failed);
|
|
|
|
|
|
|
|
|
|
// Close the pseudoconsole and wait for all output to drain.
|
|
|
|
|
_hPC.reset();
|
|
|
|
|
if (auto localOutputThreadHandle = std::move(_hOutputThread))
|
|
|
|
|
{
|
|
|
|
|
LOG_LAST_ERROR_IF(WAIT_FAILED == WaitForSingleObject(localOutputThreadHandle.get(), INFINITE));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
_indicateExitWithStatus(exitCode);
|
|
|
|
|
|
|
|
|
|
_piClient.reset();
|
2019-05-03 00:29:04 +02:00
|
|
|
|
}
|
|
|
|
|
|
2019-11-07 00:09:01 +01:00
|
|
|
|
void ConptyConnection::WriteInput(hstring const& data)
|
2019-05-03 00:29:04 +02:00
|
|
|
|
{
|
2019-11-25 23:22:29 +01:00
|
|
|
|
if (!_isConnected())
|
2019-05-03 00:29:04 +02:00
|
|
|
|
{
|
2019-11-07 00:09:01 +01:00
|
|
|
|
return;
|
2019-05-03 00:29:04 +02:00
|
|
|
|
}
|
|
|
|
|
|
2019-11-07 00:09:01 +01:00
|
|
|
|
// convert from UTF-16LE to UTF-8 as ConPty expects UTF-8
|
|
|
|
|
// TODO GH#3378 reconcile and unify UTF-8 converters
|
|
|
|
|
std::string str = winrt::to_string(data);
|
|
|
|
|
LOG_IF_WIN32_BOOL_FALSE(WriteFile(_inPipe.get(), str.c_str(), (DWORD)str.length(), nullptr, nullptr));
|
|
|
|
|
}
|
2019-05-03 00:29:04 +02:00
|
|
|
|
|
2019-11-07 00:09:01 +01:00
|
|
|
|
void ConptyConnection::Resize(uint32_t rows, uint32_t columns)
|
|
|
|
|
{
|
|
|
|
|
if (!_hPC)
|
2019-05-03 00:29:04 +02:00
|
|
|
|
{
|
2019-11-07 00:09:01 +01:00
|
|
|
|
_initialRows = rows;
|
|
|
|
|
_initialCols = columns;
|
2019-05-03 00:29:04 +02:00
|
|
|
|
}
|
2019-11-25 23:22:29 +01:00
|
|
|
|
else if (_isConnected())
|
2019-05-03 00:29:04 +02:00
|
|
|
|
{
|
2019-11-16 02:02:38 +01:00
|
|
|
|
THROW_IF_FAILED(ConptyResizePseudoConsole(_hPC.get(), { Utils::ClampToShortMax(columns, 1), Utils::ClampToShortMax(rows, 1) }));
|
2019-05-03 00:29:04 +02:00
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2020-01-03 19:44:27 +01:00
|
|
|
|
void ConptyConnection::Close() noexcept
|
2019-05-03 00:29:04 +02:00
|
|
|
|
{
|
2019-11-25 23:22:29 +01:00
|
|
|
|
if (_transitionToState(ConnectionState::Closing))
|
2019-11-07 00:09:01 +01:00
|
|
|
|
{
|
2019-11-25 23:22:29 +01:00
|
|
|
|
// EXIT POINT
|
2019-11-07 00:09:01 +01:00
|
|
|
|
_clientExitWait.reset(); // immediately stop waiting for the client to exit.
|
2019-05-03 00:29:04 +02:00
|
|
|
|
|
2019-11-25 23:22:29 +01:00
|
|
|
|
_hPC.reset(); // tear down the pseudoconsole (this is like clicking X on a console window)
|
2019-05-03 00:29:04 +02:00
|
|
|
|
|
2019-11-25 23:22:29 +01:00
|
|
|
|
_inPipe.reset(); // break the pipes
|
2019-11-07 00:09:01 +01:00
|
|
|
|
_outPipe.reset();
|
2019-05-03 00:29:04 +02:00
|
|
|
|
|
2019-11-25 23:22:29 +01:00
|
|
|
|
if (_hOutputThread)
|
|
|
|
|
{
|
|
|
|
|
// Tear down our output thread -- now that the output pipe was closed on the
|
|
|
|
|
// far side, we can run down our local reader.
|
|
|
|
|
LOG_LAST_ERROR_IF(WAIT_FAILED == WaitForSingleObject(_hOutputThread.get(), INFINITE));
|
|
|
|
|
_hOutputThread.reset();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (_piClient.hProcess)
|
|
|
|
|
{
|
|
|
|
|
// Wait for the client to terminate (which it should do successfully)
|
|
|
|
|
LOG_LAST_ERROR_IF(WAIT_FAILED == WaitForSingleObject(_piClient.hProcess, INFINITE));
|
|
|
|
|
_piClient.reset();
|
|
|
|
|
}
|
2019-05-03 00:29:04 +02:00
|
|
|
|
|
2019-11-25 23:22:29 +01:00
|
|
|
|
_transitionToState(ConnectionState::Closed);
|
2019-05-03 00:29:04 +02:00
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
DWORD ConptyConnection::_OutputThread()
|
|
|
|
|
{
|
2020-03-10 22:04:40 +01:00
|
|
|
|
// Keep us alive until the output thread terminates; the destructor
|
|
|
|
|
// won't wait for us, and the known exit points _do_.
|
|
|
|
|
auto strongThis{ get_strong() };
|
|
|
|
|
|
2019-11-07 00:09:01 +01:00
|
|
|
|
// process the data of the output pipe in a loop
|
2019-05-03 00:29:04 +02:00
|
|
|
|
while (true)
|
|
|
|
|
{
|
2020-01-30 01:55:48 +01:00
|
|
|
|
DWORD read{};
|
|
|
|
|
|
|
|
|
|
const auto readFail{ !ReadFile(_outPipe.get(), _buffer.data(), gsl::narrow_cast<DWORD>(_buffer.size()), &read, nullptr) };
|
|
|
|
|
if (readFail) // reading failed (we must check this first, because read will also be 0.)
|
|
|
|
|
{
|
|
|
|
|
const auto lastError = GetLastError();
|
|
|
|
|
if (lastError != ERROR_BROKEN_PIPE && !_isStateAtOrBeyond(ConnectionState::Closing))
|
|
|
|
|
{
|
|
|
|
|
// EXIT POINT
|
|
|
|
|
_indicateExitWithStatus(HRESULT_FROM_WIN32(lastError)); // print a message
|
|
|
|
|
_transitionToState(ConnectionState::Failed);
|
|
|
|
|
return gsl::narrow_cast<DWORD>(HRESULT_FROM_WIN32(lastError));
|
|
|
|
|
}
|
|
|
|
|
// else we call convertUTF8ChunkToUTF16 with an empty string_view to convert possible remaining partials to U+FFFD
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const HRESULT result{ til::u8u16(std::string_view{ _buffer.data(), read }, _u16Str, _u8State) };
|
|
|
|
|
if (FAILED(result))
|
2019-11-07 00:09:01 +01:00
|
|
|
|
{
|
2019-11-25 23:22:29 +01:00
|
|
|
|
if (_isStateAtOrBeyond(ConnectionState::Closing))
|
2019-11-07 00:09:01 +01:00
|
|
|
|
{
|
2019-11-25 23:22:29 +01:00
|
|
|
|
// This termination was expected.
|
2019-11-07 00:09:01 +01:00
|
|
|
|
return 0;
|
|
|
|
|
}
|
|
|
|
|
|
2019-11-25 23:22:29 +01:00
|
|
|
|
// EXIT POINT
|
|
|
|
|
_indicateExitWithStatus(result); // print a message
|
|
|
|
|
_transitionToState(ConnectionState::Failed);
|
2020-01-30 01:55:48 +01:00
|
|
|
|
return gsl::narrow_cast<DWORD>(result);
|
2019-11-07 00:09:01 +01:00
|
|
|
|
}
|
2019-05-03 00:29:04 +02:00
|
|
|
|
|
2020-01-30 01:55:48 +01:00
|
|
|
|
if (_u16Str.empty())
|
2019-11-07 00:09:01 +01:00
|
|
|
|
{
|
|
|
|
|
return 0;
|
|
|
|
|
}
|
2019-05-03 00:29:04 +02:00
|
|
|
|
|
2020-02-10 21:40:01 +01:00
|
|
|
|
if (!_receivedFirstByte)
|
2019-11-07 00:09:01 +01:00
|
|
|
|
{
|
2020-01-03 19:44:27 +01:00
|
|
|
|
const auto now = std::chrono::high_resolution_clock::now();
|
|
|
|
|
const std::chrono::duration<double> delta = now - _startTime;
|
2019-11-07 00:09:01 +01:00
|
|
|
|
|
2020-01-03 19:44:27 +01:00
|
|
|
|
#pragma warning(suppress : 26477 26485 26494 26482 26446) // We don't control TraceLoggingWrite
|
2019-11-07 00:09:01 +01:00
|
|
|
|
TraceLoggingWrite(g_hTerminalConnectionProvider,
|
2020-02-10 21:40:01 +01:00
|
|
|
|
"ReceivedFirstByte",
|
|
|
|
|
TraceLoggingDescription("An event emitted when the connection receives the first byte"),
|
2019-11-07 00:09:01 +01:00
|
|
|
|
TraceLoggingGuid(_guid, "SessionGuid", "The WT_SESSION's GUID"),
|
|
|
|
|
TraceLoggingFloat64(delta.count(), "Duration"),
|
|
|
|
|
TraceLoggingKeyword(MICROSOFT_KEYWORD_MEASURES),
|
|
|
|
|
TelemetryPrivacyDataTag(PDT_ProductAndServicePerformance));
|
2020-02-10 21:40:01 +01:00
|
|
|
|
_receivedFirstByte = true;
|
2019-11-07 00:09:01 +01:00
|
|
|
|
}
|
2019-05-03 00:29:04 +02:00
|
|
|
|
|
|
|
|
|
// Pass the output to our registered event handlers
|
2020-01-30 01:55:48 +01:00
|
|
|
|
_TerminalOutputHandlers(_u16Str);
|
2019-05-03 00:29:04 +02:00
|
|
|
|
}
|
2019-11-07 00:09:01 +01:00
|
|
|
|
|
|
|
|
|
return 0;
|
2019-05-03 00:29:04 +02:00
|
|
|
|
}
|
2019-11-07 00:09:01 +01:00
|
|
|
|
|
2019-05-03 00:29:04 +02:00
|
|
|
|
}
|