AzureConnection: remove our dependency on cpprestsdk (#14776)

This pull request removes, in full, our dependency on cpprestsdk. This
allows us to shed 500KiB-1.2MiB from our package off the top and enables
the following future investments:

- Removal of the App CRT forwarders to save an additional ~500KiB
- Switching over to the HybridCRT and removing our dependency on _any
  CRT_.

cpprest was built on my dev box two or so years ago, and is in _utter_
violation of our compliance guidelines on SBOM et al.

In addition, this change allows us to use the proxy server configured
in Windows Settings.

I did this in four steps (represented roughly by the individual commits):

1. Switch from cpprest's http_client/json to Windows.Web.Http and
   Windows.Data.Json
2. Switch from websocketpp to winhttp.dll's WebSocket implementation¹
3. Remove all remaining utility classes
4. Purge all dependencies from all projects and scripts on cpprest.

I also took this opportunity to add a feature flag that allows Dev
builds to run AzureConnection in-process.

¹ Windows.Networking.Sockets' API is so unergonomic that it was simply
infeasible (and also _horrible_) to use it.

## Validation Steps

I've run the Azure Connection quite a bit inproc.

Closes #4575.
Might be related to #5977, #11714, and with the user agent thing maybe #14403.
This commit is contained in:
Dustin L. Howett 2023-02-07 15:13:10 -06:00 коммит произвёл GitHub
Родитель 42e8de3b52
Коммит 4903cfd484
Не найден ключ, соответствующий данной подписи
Идентификатор ключа GPG: 4AEE18F83AFDEB23
16 изменённых файлов: 293 добавлений и 348 удалений

5
.github/actions/spelling/allow/apis.txt поставляемый
Просмотреть файл

@ -55,6 +55,8 @@ GETMOUSEHOVERTIME
Hashtable
HIGHCONTRASTON
HIGHCONTRASTW
hinternet
HINTERNET
hotkeys
href
hrgn
@ -214,6 +216,8 @@ Viewbox
virtualalloc
wcsstr
wcstoui
WDJ
winhttp
winmain
winsta
winstamin
@ -221,6 +225,7 @@ wmemcmp
wpc
WSF
wsregex
WWH
wwinmain
xchg
XDocument

3
.github/actions/spelling/expect/expect.txt поставляемый
Просмотреть файл

@ -311,7 +311,6 @@ CPLINFO
cplusplus
CPPCORECHECK
cppcorecheckrules
cpprest
cpprestsdk
cppwinrt
CProc
@ -1437,7 +1436,6 @@ PPEB
ppf
ppguid
ppidl
pplx
PPROC
PPROCESS
ppropvar
@ -2114,7 +2112,6 @@ WDDMCONSOLECONTEXT
wdm
webpage
websites
websockets
wekyb
wex
wextest

Просмотреть файл

@ -67,51 +67,6 @@
}
]
}
},
{
// THIRD PARTY SOFTWARE
"MatchedPath": [
"cpprest*.dll"
],
"SigningInfo": {
"Operations": [
{
"KeyCode": "CP-231522",
"OperationSetCode": "SigntoolSign",
"Parameters": [
{
"parameterName": "OpusName",
"parameterValue": "Microsoft"
},
{
"parameterName": "OpusInfo",
"parameterValue": "http://www.microsoft.com"
},
{
"parameterName": "FileDigest",
"parameterValue": "/fd \"SHA256\""
},
{
"parameterName": "PageHash",
"parameterValue": "/NPH"
},
{
"parameterName": "TimeStamp",
"parameterValue": "/tr \"http://rfc3161.gtm.corp.microsoft.com/TSS/HttpTspServer\" /td sha256"
}
],
"ToolName": "sign",
"ToolVersion": "1.0"
},
{
"KeyCode": "CP-231522",
"OperationSetCode": "SigntoolVerify",
"Parameters": [],
"ToolName": "sign",
"ToolVersion": "1.0"
}
]
}
}
]
}

Просмотреть файл

@ -64,7 +64,7 @@ parameters:
variables:
MakeAppxPath: 'C:\Program Files (x86)\Windows Kits\10\bin\10.0.22621.0\x86\MakeAppx.exe'
TerminalInternalPackageVersion: "0.0.7"
TerminalInternalPackageVersion: "0.0.8"
# If we are building a branch called "release-*", change the NuGet suffix
# to "preview". If we don't do that, XES will set the suffix to "release1"
# because it truncates the value after the first period.

Просмотреть файл

@ -144,7 +144,7 @@ jobs:
inputs:
TargetPattern: guardianGlob
# See https://aka.ms/gdn-globs for how to do match patterns
AnalyzeTargetGlob: $(Build.SourcesDirectory)\bin\**\*.dll;$(Build.SourcesDirectory)\bin\**\*.exe;-:file|**\Microsoft.UI.Xaml.dll;-:file|**\Microsoft.Toolkit.Win32.UI.XamlHost.dll;-:file|**\vcruntime*.dll;-:file|**\vcomp*.dll;-:file|**\vccorlib*.dll;-:file|**\vcamp*.dll;-:file|**\msvcp*.dll;-:file|**\concrt*.dll;-:file|**\TerminalThemeHelpers*.dll;-:file|**\cpprest*.dll
AnalyzeTargetGlob: $(Build.SourcesDirectory)\bin\**\*.dll;$(Build.SourcesDirectory)\bin\**\*.exe;-:file|**\Microsoft.UI.Xaml.dll;-:file|**\Microsoft.Toolkit.Win32.UI.XamlHost.dll;-:file|**\vcruntime*.dll;-:file|**\vcomp*.dll;-:file|**\vccorlib*.dll;-:file|**\vcamp*.dll;-:file|**\msvcp*.dll;-:file|**\concrt*.dll;-:file|**\TerminalThemeHelpers*.dll
continueOnError: true
# Set XES_SERIALPOSTBUILDREADY to run Security and Compliance task once per build

Просмотреть файл

@ -96,11 +96,6 @@ Try {
Throw "Failed to find App.xbf (TerminalApp project) in resources.pri"
}
If (($null -eq (Get-Item "$AppxPackageRootPath\cpprest142_2_10.dll" -EA:Ignore)) -And
($null -eq (Get-Item "$AppxPackageRootPath\cpprest142_2_10d.dll" -EA:Ignore))) {
Throw "Failed to find cpprest142_2_10.dll -- check the WAP packaging project"
}
If (($null -eq (Get-Item "$AppxPackageRootPath\wtd.exe" -EA:Ignore)) -And
($null -eq (Get-Item "$AppxPackageRootPath\wt.exe" -EA:Ignore))) {
Throw "Failed to find wt.exe/wtd.exe -- check the WAP packaging project"

Просмотреть файл

@ -5,7 +5,6 @@
<package id="Microsoft.Internal.PGO-Helpers.Cpp" version="0.2.34" targetFramework="native" />
<package id="Microsoft.Taef" version="10.60.210621002" targetFramework="native" />
<package id="Microsoft.Windows.CppWinRT" version="2.0.210825.3" targetFramework="native" />
<package id="vcpkg-cpprestsdk" version="2.10.14" targetFramework="native" />
<package id="Microsoft.VCRTForwarders.140" version="1.0.4" targetFramework="native" />
<package id="Microsoft.Internal.Windows.Terminal.ThemeHelpers" version="0.6.220404001" targetFramework="native" />
<package id="Microsoft.VisualStudio.Setup.Configuration.Native" version="2.3.2262" targetFramework="native" developmentDependency="true" />

Просмотреть файл

@ -197,26 +197,6 @@
<!-- **END VC LIBS HACK** -->
<!-- **BEGIN TERMINAL CONNECTION HACK** -->
<!-- This is the same as the above VC libs hack, but for TerminalConnection.
TerminalConnection depends on cpprest*.dll, and if we don't include it in
the packaging output, we'll crash as soon as we try to load
TerminalConnection.dll.
The Sample sln needs to do this manually - the real exe has a
ProjectReference to TerminalConnection.vcxproj and can figure this out on
its own. -->
<ItemGroup>
<_TerminalConnectionDlls Include="$(OpenConsoleCommonOutDir)\TerminalConnection\*.dll" />
<PackagingOutputs Include="@(_TerminalConnectionDlls)">
<ProjectName>$(ProjectName)</ProjectName>
<OutputGroup>BuiltProjectOutputGroup</OutputGroup>
<TargetPath>%(Filename)%(Extension)</TargetPath>
</PackagingOutputs>
</ItemGroup>
<!-- **END TERMINAL CONNECTION HACK** -->
<!-- Same thing again here, with WindowsTerminal.exe -->
<ItemGroup>
<_WindowsTerminalExe Include="$(OpenConsoleCommonOutDir)\WindowsTerminal\*.exe" />

Просмотреть файл

@ -1247,10 +1247,17 @@ namespace winrt::TerminalApp::implementation
if (connectionType == TerminalConnection::AzureConnection::ConnectionType() &&
TerminalConnection::AzureConnection::IsAzureConnectionAvailable())
{
// TODO GH#4661: Replace this with directly using the AzCon when our VT is better
std::filesystem::path azBridgePath{ wil::GetModuleFileNameW<std::wstring>(nullptr) };
azBridgePath.replace_filename(L"TerminalAzBridge.exe");
connection = TerminalConnection::ConptyConnection();
if constexpr (Feature_AzureConnectionInProc::IsEnabled())
{
connection = TerminalConnection::AzureConnection{};
}
else
{
connection = TerminalConnection::ConptyConnection{};
}
auto valueSet = TerminalConnection::ConptyConnection::CreateSettings(azBridgePath.wstring(),
L".",
L"Azure",

Просмотреть файл

@ -3,8 +3,6 @@
#pragma once
#include "cpprest/json.h"
namespace Microsoft::Terminal::Azure
{
class AzureException : public std::runtime_error
@ -12,14 +10,24 @@ namespace Microsoft::Terminal::Azure
std::wstring _code;
public:
static bool IsErrorPayload(const web::json::value& errorObject)
static bool IsErrorPayload(const winrt::Windows::Data::Json::JsonObject& errorObject)
{
return errorObject.has_string_field(L"error");
if (!errorObject.HasKey(L"error"))
{
return false;
}
if (errorObject.GetNamedValue(L"error").ValueType() != winrt::Windows::Data::Json::JsonValueType::String)
{
return false;
}
return true;
}
AzureException(const web::json::value& errorObject) :
runtime_error(til::u16u8(errorObject.at(L"error_description").as_string())), // surface the human-readable description as .what()
_code(errorObject.at(L"error").as_string())
AzureException(const winrt::Windows::Data::Json::JsonObject& errorObject) :
runtime_error(til::u16u8(errorObject.GetNamedString(L"error_description"))), // surface the human-readable description as .what()
_code(errorObject.GetNamedString(L"error"))
{
}

Просмотреть файл

@ -15,22 +15,24 @@
#include "winrt/Windows.System.UserProfile.h"
#include "../../types/inc/Utils.hpp"
#include "winrt/Windows.Web.Http.Filters.h"
using namespace ::Microsoft::Console;
using namespace ::Microsoft::Terminal::Azure;
using namespace web;
using namespace web::http;
using namespace web::http::client;
using namespace web::websockets::client;
using namespace winrt::Windows::Security::Credentials;
static constexpr int CurrentCredentialVersion = 1;
static constexpr auto PasswordVaultResourceName = L"Terminal";
static constexpr auto HttpUserAgent = L"Terminal/0.0";
static constexpr int CurrentCredentialVersion = 2;
static constexpr std::wstring_view PasswordVaultResourceName = L"Terminal";
static constexpr std::wstring_view HttpUserAgent = L"Mozilla/5.0 (Windows NT 10.0) Terminal/1.0";
static constexpr int USER_INPUT_COLOR = 93; // yellow - the color of something the user can type
static constexpr int USER_INFO_COLOR = 97; // white - the color of clarifying information
using namespace winrt::Windows::Foundation;
namespace WDJ = ::winrt::Windows::Data::Json;
namespace WSS = ::winrt::Windows::Storage::Streams;
namespace WWH = ::winrt::Windows::Web::Http;
static constexpr winrt::guid AzureConnectionType = { 0xd9fcfdfa, 0xa479, 0x412c, { 0x83, 0xb7, 0xc5, 0x64, 0xe, 0x61, 0xcd, 0x62 } };
static inline std::wstring _colorize(const unsigned int colorCode, const std::wstring_view text)
@ -115,6 +117,8 @@ namespace winrt::Microsoft::Terminal::TerminalConnection::implementation
// - creates the output thread (where we will do the authentication and actually connect to Azure)
void AzureConnection::Start()
{
_httpClient = winrt::Windows::Web::Http::HttpClient{};
_httpClient.DefaultRequestHeaders().UserAgent().TryParseAdd(HttpUserAgent);
// Create our own output handling thread
// Each connection needs to make sure to drain the output from its backing host.
_hOutputThread.reset(CreateThread(
@ -126,7 +130,7 @@ namespace winrt::Microsoft::Terminal::TerminalConnection::implementation
{
return pInstance->_OutputThread();
}
return gsl::narrow_cast<DWORD>(E_INVALIDARG);
return gsl::narrow<DWORD>(E_INVALIDARG);
},
this,
0,
@ -183,12 +187,8 @@ namespace winrt::Microsoft::Terminal::TerminalConnection::implementation
if (_state == AzureState::TermConnected)
{
// If we're connected, we don't need to do any fun input shenanigans.
websocket_outgoing_message msg;
const auto str = winrt::to_string(data);
msg.set_utf8_message(str);
_cloudShellSocket.send(msg).get();
auto buff{ winrt::to_string(data) };
WinHttpWebSocketSend(_webSocket.get(), WINHTTP_WEB_SOCKET_UTF8_MESSAGE_BUFFER_TYPE, buff.data(), gsl::narrow<DWORD>(buff.size()));
return;
}
@ -238,16 +238,17 @@ namespace winrt::Microsoft::Terminal::TerminalConnection::implementation
}
else // We only transition to Connected when we've established the websocket.
{
// Initialize client
http_client terminalClient(_cloudShellUri);
auto uri{ fmt::format(L"{}terminals/{}/size?cols={}&rows={}&version=2019-01-01", _cloudShellUri, _terminalID, columns, rows) };
// Initialize the request
http_request terminalRequest(L"POST");
terminalRequest.set_request_uri(fmt::format(L"terminals/{}/size?cols={}&rows={}&version=2019-01-01", _terminalID, columns, rows));
terminalRequest.set_body(json::value::null());
WWH::HttpStringContent content{
L"",
WSS::UnicodeEncoding::Utf8,
// LOAD-BEARING. the API returns "'content-type' should be 'application/json' or 'multipart/form-data'"
L"application/json"
};
// Send the request (don't care about the response)
(void)_SendAuthenticatedRequestReturningJson(terminalClient, terminalRequest);
std::ignore = _SendRequestReturningJson(uri, content);
}
}
CATCH_LOG();
@ -264,7 +265,10 @@ namespace winrt::Microsoft::Terminal::TerminalConnection::implementation
if (_state == AzureState::TermConnected)
{
// Close the websocket connection
_cloudShellSocket.close();
std::ignore = WinHttpWebSocketClose(_webSocket.get(), WINHTTP_WEB_SOCKET_SUCCESS_CLOSE_STATUS, nullptr, 0); // throw away the error
_webSocket.reset();
_socketConnectionHandle.reset();
_socketSessionHandle.reset();
}
if (_hOutputThread)
@ -287,44 +291,46 @@ namespace winrt::Microsoft::Terminal::TerminalConnection::implementation
// - tenant - the unparsed tenant
// Return value:
// - a tuple containing the ID and display name of the tenant.
static Tenant _crackTenant(const json::value& jsonTenant)
static Tenant _crackTenant(const WDJ::IJsonValue& value)
{
auto jsonTenant{ value.GetObjectW() };
Tenant tenant{};
if (jsonTenant.has_string_field(L"tenantID"))
if (jsonTenant.HasKey(L"tenantID"))
{
// for compatibility with version 1 credentials
tenant.ID = jsonTenant.at(L"tenantID").as_string();
tenant.ID = jsonTenant.GetNamedString(L"tenantID");
}
else
{
// This one comes in off the wire
tenant.ID = jsonTenant.at(L"tenantId").as_string();
tenant.ID = jsonTenant.GetNamedString(L"tenantId");
}
if (jsonTenant.has_string_field(L"displayName"))
if (jsonTenant.HasKey(L"displayName"))
{
tenant.DisplayName = jsonTenant.at(L"displayName").as_string();
tenant.DisplayName = jsonTenant.GetNamedString(L"displayName");
}
if (jsonTenant.has_string_field(L"defaultDomain"))
if (jsonTenant.HasKey(L"defaultDomain"))
{
tenant.DefaultDomain = jsonTenant.at(L"defaultDomain").as_string();
tenant.DefaultDomain = jsonTenant.GetNamedString(L"defaultDomain");
}
return tenant;
}
static void _packTenant(json::value& jsonTenant, const Tenant& tenant)
static void _packTenant(const WDJ::JsonObject& jsonTenant, const Tenant& tenant)
{
jsonTenant[L"tenantId"] = json::value::string(tenant.ID);
jsonTenant.SetNamedValue(L"tenantId", WDJ::JsonValue::CreateStringValue(tenant.ID));
if (tenant.DisplayName.has_value())
{
jsonTenant[L"displayName"] = json::value::string(*tenant.DisplayName);
jsonTenant.SetNamedValue(L"displayName", WDJ::JsonValue::CreateStringValue(*tenant.DisplayName));
}
if (tenant.DefaultDomain.has_value())
{
jsonTenant[L"defaultDomain"] = json::value::string(*tenant.DefaultDomain);
jsonTenant.SetNamedValue(L"defaultDomain", WDJ::JsonValue::CreateStringValue(*tenant.DefaultDomain));
}
}
@ -383,35 +389,42 @@ namespace winrt::Microsoft::Terminal::TerminalConnection::implementation
case AzureState::TermConnected:
{
_transitionToState(ConnectionState::Connected);
while (true)
{
// Read from websocket
pplx::task<websocket_incoming_message> msgT;
try
WINHTTP_WEB_SOCKET_BUFFER_TYPE bufferType{};
DWORD read{};
THROW_IF_WIN32_ERROR(WinHttpWebSocketReceive(_webSocket.get(), _buffer.data(), gsl::narrow<DWORD>(_buffer.size()), &read, &bufferType));
switch (bufferType)
{
msgT = _cloudShellSocket.receive();
msgT.wait();
case WINHTTP_WEB_SOCKET_UTF8_FRAGMENT_BUFFER_TYPE:
case WINHTTP_WEB_SOCKET_UTF8_MESSAGE_BUFFER_TYPE:
{
const auto result{ til::u8u16(std::string_view{ _buffer.data(), read }, _u16Str, _u8State) };
if (FAILED(result))
{
// EXIT POINT
_transitionToState(ConnectionState::Failed);
return gsl::narrow<DWORD>(result);
}
if (_u16Str.empty())
{
continue;
}
// Pass the output to our registered event handlers
_TerminalOutputHandlers(_u16Str);
break;
}
catch (...)
{
// Websocket has been closed; consider it a graceful exit?
// This should result in our termination.
case WINHTTP_WEB_SOCKET_CLOSE_BUFFER_TYPE:
// EXIT POINT
if (_transitionToState(ConnectionState::Closed))
{
// End the output thread.
return S_FALSE;
}
}
auto msg = msgT.get();
auto msgStringTask = msg.extract_string();
auto msgString = msgStringTask.get();
// Convert to hstring
const auto hstr = winrt::to_hstring(msgString);
// Pass the output to our registered event handlers
_TerminalOutputHandlers(hstr);
}
return S_OK;
}
@ -449,25 +462,29 @@ namespace winrt::Microsoft::Terminal::TerminalConnection::implementation
_tenantList.clear();
for (const auto& entry : credList)
{
auto nameJson = json::value::parse(entry.UserName().c_str());
std::optional<int> credentialVersion;
if (nameJson.has_integer_field(U("ver")))
try
{
credentialVersion = nameJson.at(U("ver")).as_integer();
auto nameJson = WDJ::JsonObject::Parse(entry.UserName());
std::optional<int> credentialVersion;
if (nameJson.HasKey(L"ver"))
{
credentialVersion = static_cast<int>(nameJson.GetNamedNumber(L"ver"));
}
if (!credentialVersion.has_value() || credentialVersion.value() != CurrentCredentialVersion)
{
// ignore credentials that aren't from the latest credential revision
vault.Remove(entry);
oldVersionEncountered = true;
continue;
}
auto newTenant{ _tenantList.emplace_back(_crackTenant(nameJson)) };
_WriteStringWithNewline(_formatTenant(numTenants, newTenant));
numTenants++;
}
if (!credentialVersion.has_value() || credentialVersion.value() != CurrentCredentialVersion)
{
// ignore credentials that aren't from the latest credential revision
vault.Remove(entry);
oldVersionEncountered = true;
continue;
}
auto newTenant{ _tenantList.emplace_back(_crackTenant(nameJson)) };
_WriteStringWithNewline(_formatTenant(numTenants, newTenant));
numTenants++;
CATCH_LOG();
}
if (!numTenants)
@ -533,11 +550,11 @@ namespace winrt::Microsoft::Terminal::TerminalConnection::implementation
// User wants to login with one of the saved connection settings
auto desiredCredential = credList.GetAt(selectedTenant);
desiredCredential.RetrievePassword();
auto passWordJson = json::value::parse(desiredCredential.Password().c_str());
auto passWordJson = WDJ::JsonObject::Parse(desiredCredential.Password());
_currentTenant = til::at(_tenantList, selectedTenant); // we already unpacked the name info, so we should just use it
_accessToken = passWordJson.at(L"accessToken").as_string();
_refreshToken = passWordJson.at(L"refreshToken").as_string();
_expiry = std::stoi(passWordJson.at(L"expiry").as_string());
_setAccessToken(passWordJson.GetNamedString(L"accessToken"));
_refreshToken = passWordJson.GetNamedString(L"refreshToken");
_expiry = std::stoi(winrt::to_string(passWordJson.GetNamedString(L"expiry")));
const auto t1 = std::chrono::system_clock::now();
const auto timeNow = std::chrono::duration_cast<std::chrono::seconds>(t1.time_since_epoch()).count();
@ -577,17 +594,17 @@ namespace winrt::Microsoft::Terminal::TerminalConnection::implementation
const auto deviceCodeResponse = _GetDeviceCode();
// Print the message and store the device code, polling interval and expiry
const auto message = winrt::to_hstring(deviceCodeResponse.at(L"message").as_string().c_str());
const auto message{ deviceCodeResponse.GetNamedString(L"message") };
_WriteStringWithNewline(message);
_WriteStringWithNewline(RS_(L"AzureCodeExpiry"));
const auto devCode = deviceCodeResponse.at(L"device_code").as_string();
const auto pollInterval = std::stoi(deviceCodeResponse.at(L"interval").as_string());
const auto expiresIn = std::stoi(deviceCodeResponse.at(L"expires_in").as_string());
const auto devCode = deviceCodeResponse.GetNamedString(L"device_code");
const auto pollInterval = std::stoi(winrt::to_string(deviceCodeResponse.GetNamedString(L"interval")));
const auto expiresIn = std::stoi(winrt::to_string(deviceCodeResponse.GetNamedString(L"expires_in")));
// Wait for user authentication and obtain the access/refresh tokens
auto authenticatedResponse = _WaitForUser(devCode, pollInterval, expiresIn);
_accessToken = authenticatedResponse.at(L"access_token").as_string();
_refreshToken = authenticatedResponse.at(L"refresh_token").as_string();
_setAccessToken(authenticatedResponse.GetNamedString(L"access_token"));
_refreshToken = authenticatedResponse.GetNamedString(L"refresh_token");
// Get the tenants and the required tenant id
_PopulateTenantList();
@ -696,19 +713,18 @@ namespace winrt::Microsoft::Terminal::TerminalConnection::implementation
// - Helper function to parse the preferred shell type from user settings returned by cloud console API.
// We need this function because the field might be missing in the settings
// created with old versions of cloud console API.
std::optional<utility::string_t> AzureConnection::_ParsePreferredShellType(const web::json::value& settingsResponse)
winrt::hstring AzureConnection::_ParsePreferredShellType(const WDJ::JsonObject& settingsResponse)
{
if (settingsResponse.has_object_field(L"properties"))
if (settingsResponse.HasKey(L"properties"))
{
const auto userSettings = settingsResponse.at(L"properties");
if (userSettings.has_string_field(L"preferredShellType"))
const auto userSettings = settingsResponse.GetNamedObject(L"properties");
if (userSettings.HasKey(L"preferredShellType"))
{
const auto preferredShellTypeValue = userSettings.at(L"preferredShellType");
return preferredShellTypeValue.as_string();
return userSettings.GetNamedString(L"preferredShellType");
}
}
return std::nullopt;
return L"pwsh";
}
// Method description:
@ -717,7 +733,7 @@ namespace winrt::Microsoft::Terminal::TerminalConnection::implementation
{
// Get user's cloud shell settings
const auto settingsResponse = _GetCloudShellUserSettings();
if (settingsResponse.has_field(L"error"))
if (settingsResponse.HasKey(L"error"))
{
_WriteStringWithNewline(RS_(L"AzureNoCloudAccount"));
_transitionToState(ConnectionState::Failed);
@ -732,12 +748,36 @@ namespace winrt::Microsoft::Terminal::TerminalConnection::implementation
// Request for a terminal for said cloud shell
const auto shellType = _ParsePreferredShellType(settingsResponse);
_WriteStringWithNewline(RS_(L"AzureRequestingTerminal"));
const auto socketUri = _GetTerminal(shellType.value_or(L"pwsh"));
const auto socketUri = _GetTerminal(shellType);
_TerminalOutputHandlers(L"\r\n");
// Step 8: connecting to said terminal
const auto connReqTask = _cloudShellSocket.connect(socketUri);
connReqTask.wait();
//// Step 8: connecting to said terminal
{
wil::unique_winhttp_hinternet sessionHandle, connectionHandle, requestHandle, socketHandle;
Uri parsedUri{ socketUri };
sessionHandle.reset(WinHttpOpen(HttpUserAgent.data(), WINHTTP_ACCESS_TYPE_AUTOMATIC_PROXY, nullptr, nullptr, 0));
THROW_LAST_ERROR_IF(!sessionHandle);
connectionHandle.reset(WinHttpConnect(sessionHandle.get(), parsedUri.Host().c_str(), INTERNET_DEFAULT_HTTPS_PORT, 0));
THROW_LAST_ERROR_IF(!connectionHandle);
requestHandle.reset(WinHttpOpenRequest(connectionHandle.get(), L"GET", parsedUri.Path().c_str(), nullptr, nullptr, nullptr, WINHTTP_FLAG_SECURE));
THROW_LAST_ERROR_IF(!requestHandle);
THROW_IF_WIN32_BOOL_FALSE(WinHttpSetOption(requestHandle.get(), WINHTTP_OPTION_UPGRADE_TO_WEB_SOCKET, nullptr, 0));
#pragma warning(suppress : 26477) // WINHTTP_NO_ADDITIONAL_HEADERS expands to NULL rather than nullptr (who would have thought?)
THROW_IF_WIN32_BOOL_FALSE(WinHttpSendRequest(requestHandle.get(), WINHTTP_NO_ADDITIONAL_HEADERS, 0, nullptr, 0, 0, 0));
THROW_IF_WIN32_BOOL_FALSE(WinHttpReceiveResponse(requestHandle.get(), nullptr));
socketHandle.reset(WinHttpWebSocketCompleteUpgrade(requestHandle.get(), 0));
THROW_LAST_ERROR_IF(!socketHandle);
requestHandle.reset(); // We no longer need the request once we've upgraded it.
// We have to keep the socket session and connection handles.
_socketSessionHandle = std::move(sessionHandle);
_socketConnectionHandle = std::move(connectionHandle);
_webSocket = std::move(socketHandle);
}
_state = AzureState::TermConnected;
@ -752,58 +792,52 @@ namespace winrt::Microsoft::Terminal::TerminalConnection::implementation
// Method description:
// - helper function to send requests with default headers and extract responses as json values
// Arguments:
// - a http_client
// - a http_request for the client to send
// - the URI
// - optional body content
// - an optional HTTP method (defaults to POST if content is present, GET otherwise)
// Return value:
// - the response from the server as a json value
json::value AzureConnection::_SendRequestReturningJson(http_client& theClient, http_request theRequest)
WDJ::JsonObject AzureConnection::_SendRequestReturningJson(std::wstring_view uri, const WWH::IHttpContent& content, WWH::HttpMethod method)
{
auto& headers{ theRequest.headers() };
headers.add(L"User-Agent", HttpUserAgent);
headers.add(L"Accept", L"application/json");
if (!method)
{
method = content == nullptr ? WWH::HttpMethod::Get() : WWH::HttpMethod::Post();
}
json::value jsonResult;
const auto responseTask = theClient.request(theRequest);
responseTask.wait();
const auto response = responseTask.get();
const auto responseJsonTask = response.extract_json();
responseJsonTask.wait();
jsonResult = responseJsonTask.get();
WWH::HttpRequestMessage request{ method, Uri{ uri } };
request.Content(content);
auto headers{ request.Headers() };
headers.Accept().TryParseAdd(L"application/json");
const auto response{ _httpClient.SendRequestAsync(request).get() };
const auto string{ response.Content().ReadAsStringAsync().get() };
const auto jsonResult{ WDJ::JsonObject::Parse(string) };
THROW_IF_AZURE_ERROR(jsonResult);
return jsonResult;
}
// Method description:
// - helper function to send _authenticated_ requests with json bodies whose responses are expected
// to be json. builds on _SendRequestReturningJson.
// Arguments:
// - the http_request
json::value AzureConnection::_SendAuthenticatedRequestReturningJson(http_client& theClient, http_request theRequest)
void AzureConnection::_setAccessToken(std::wstring_view accessToken)
{
auto& headers{ theRequest.headers() };
headers.add(L"Authorization", L"Bearer " + _accessToken);
return _SendRequestReturningJson(theClient, std::move(theRequest));
_accessToken = accessToken;
_httpClient.DefaultRequestHeaders().Authorization(WWH::Headers::HttpCredentialsHeaderValue{ L"Bearer", _accessToken });
}
// Method description:
// - helper function to start the device code flow
// Return value:
// - the response to the device code flow initiation
json::value AzureConnection::_GetDeviceCode()
WDJ::JsonObject AzureConnection::_GetDeviceCode()
{
// Initialize the client
http_client loginClient(_loginUri);
// Initialize the request
http_request commonRequest(L"POST");
commonRequest.set_request_uri(L"common/oauth2/devicecode");
const auto body{ fmt::format(L"client_id={}&resource={}", AzureClientID, _wantedResource) };
commonRequest.set_body(body.c_str(), L"application/x-www-form-urlencoded");
// Send the request and receive the response as a json value
return _SendRequestReturningJson(loginClient, commonRequest);
auto uri{ fmt::format(L"{}common/oauth2/devicecode", _loginUri) };
WWH::HttpFormUrlEncodedContent content{
std::unordered_map<winrt::hstring, winrt::hstring>{
{ winrt::hstring{ L"client_id" }, winrt::hstring{ AzureClientID } },
{ winrt::hstring{ L"resource" }, winrt::hstring{ _wantedResource } },
}
};
return _SendRequestReturningJson(uri, content);
}
// Method description:
@ -815,14 +849,17 @@ namespace winrt::Microsoft::Terminal::TerminalConnection::implementation
// Return value:
// - if authentication is done successfully, then return the response from the server
// - else, throw an exception
json::value AzureConnection::_WaitForUser(const utility::string_t deviceCode, int pollInterval, int expiresIn)
WDJ::JsonObject AzureConnection::_WaitForUser(const winrt::hstring& deviceCode, int pollInterval, int expiresIn)
{
// Initialize the client
http_client pollingClient(_loginUri);
// Continuously send a poll request until the user authenticates
const auto body{ fmt::format(L"grant_type=device_code&resource={}&client_id={}&code={}", _wantedResource, AzureClientID, deviceCode) };
const auto requestUri = L"common/oauth2/token";
auto uri{ fmt::format(L"{}common/oauth2/token", _loginUri) };
WWH::HttpFormUrlEncodedContent content{
std::unordered_map<winrt::hstring, winrt::hstring>{
{ winrt::hstring{ L"grant_type" }, winrt::hstring{ L"device_code" } },
{ winrt::hstring{ L"client_id" }, winrt::hstring{ AzureClientID } },
{ winrt::hstring{ L"resource" }, winrt::hstring{ _wantedResource } },
{ winrt::hstring{ L"code" }, deviceCode },
}
};
// use a steady clock here so it's not impacted by local time discontinuities
const auto tokenExpiry{ std::chrono::steady_clock::now() + std::chrono::seconds(expiresIn) };
@ -837,14 +874,11 @@ namespace winrt::Microsoft::Terminal::TerminalConnection::implementation
break;
}
http_request pollRequest(L"POST");
pollRequest.set_request_uri(requestUri);
pollRequest.set_body(body.c_str(), L"application/x-www-form-urlencoded");
try
{
auto response{ _SendRequestReturningJson(pollingClient, pollRequest) };
auto response = _SendRequestReturningJson(uri, content);
_WriteStringWithNewline(RS_(L"AzureSuccessfullyAuthenticated"));
// Got a valid response: we're done
return response;
}
@ -859,7 +893,7 @@ namespace winrt::Microsoft::Terminal::TerminalConnection::implementation
} // uncaught exceptions bubble up to the caller
}
return json::value::null();
return nullptr;
}
// Method description:
@ -868,16 +902,11 @@ namespace winrt::Microsoft::Terminal::TerminalConnection::implementation
// - the response which contains a list of the user's Azure tenants
void AzureConnection::_PopulateTenantList()
{
// Initialize the client
http_client tenantClient(_resourceUri);
// Initialize the request
http_request tenantRequest(L"GET");
tenantRequest.set_request_uri(L"tenants?api-version=2020-01-01");
auto uri{ fmt::format(L"{}tenants?api-version=2020-01-01", _resourceUri) };
// Send the request and return the response as a json value
auto tenantResponse{ _SendAuthenticatedRequestReturningJson(tenantClient, tenantRequest) };
auto tenantList{ tenantResponse.at(L"value").as_array() };
auto tenantResponse{ _SendRequestReturningJson(uri, nullptr) };
auto tenantList{ tenantResponse.GetNamedArray(L"value") };
_tenantList.clear();
std::transform(tenantList.begin(), tenantList.end(), std::back_inserter(_tenantList), _crackTenant);
@ -889,82 +918,73 @@ namespace winrt::Microsoft::Terminal::TerminalConnection::implementation
// - the response with the new tokens
void AzureConnection::_RefreshTokens()
{
// Initialize the client
http_client refreshClient(_loginUri);
// Initialize the request
http_request refreshRequest(L"POST");
refreshRequest.set_request_uri(_currentTenant->ID + L"/oauth2/token");
const auto body{ fmt::format(L"client_id={}&resource={}&grant_type=refresh_token&refresh_token={}", AzureClientID, _wantedResource, _refreshToken) };
refreshRequest.set_body(body.c_str(), L"application/x-www-form-urlencoded");
auto uri{ fmt::format(L"{}{}/oauth2/token", _loginUri, _currentTenant->ID) };
WWH::HttpFormUrlEncodedContent content{
std::unordered_map<winrt::hstring, winrt::hstring>{
{ winrt::hstring{ L"grant_type" }, winrt::hstring{ L"refresh_token" } },
{ winrt::hstring{ L"client_id" }, winrt::hstring{ AzureClientID } },
{ winrt::hstring{ L"resource" }, winrt::hstring{ _wantedResource } },
{ winrt::hstring{ L"refresh_token" }, winrt::hstring{ _refreshToken } },
}
};
// Send the request and return the response as a json value
auto refreshResponse{ _SendRequestReturningJson(refreshClient, refreshRequest) };
_accessToken = refreshResponse.at(L"access_token").as_string();
_refreshToken = refreshResponse.at(L"refresh_token").as_string();
_expiry = std::stoi(refreshResponse.at(L"expires_on").as_string());
auto refreshResponse{ _SendRequestReturningJson(uri, content) };
_setAccessToken(refreshResponse.GetNamedString(L"access_token"));
_refreshToken = refreshResponse.GetNamedString(L"refresh_token");
_expiry = std::stoi(winrt::to_string(refreshResponse.GetNamedString(L"expires_on")));
}
// Method description:
// - helper function to get the user's cloud shell settings
// Return value:
// - the user's cloud shell settings
json::value AzureConnection::_GetCloudShellUserSettings()
WDJ::JsonObject AzureConnection::_GetCloudShellUserSettings()
{
// Initialize client
http_client settingsClient(_resourceUri);
// Initialize request
http_request settingsRequest(L"GET");
settingsRequest.set_request_uri(L"providers/Microsoft.Portal/userSettings/cloudconsole?api-version=2018-10-01");
return _SendAuthenticatedRequestReturningJson(settingsClient, settingsRequest);
auto uri{ fmt::format(L"{}providers/Microsoft.Portal/userSettings/cloudconsole?api-version=2020-04-01-preview", _resourceUri) };
return _SendRequestReturningJson(uri, nullptr);
}
// Method description:
// - helper function to request for a cloud shell
// Return value:
// - the uri for the cloud shell
utility::string_t AzureConnection::_GetCloudShell()
winrt::hstring AzureConnection::_GetCloudShell()
{
// Initialize client
http_client cloudShellClient(_resourceUri);
auto uri{ fmt::format(L"{}providers/Microsoft.Portal/consoles/default?api-version=2020-04-01-preview", _resourceUri) };
// Initialize request
http_request shellRequest(L"PUT");
shellRequest.set_request_uri(L"providers/Microsoft.Portal/consoles/default?api-version=2018-10-01");
// { "properties": { "osType": "linux" } }
auto body = json::value::object({ { U("properties"), json::value::object({ { U("osType"), json::value::string(U("linux")) } }) } });
shellRequest.set_body(body);
WWH::HttpStringContent content{
LR"-({"properties": {"osType": "linux"}})-",
WSS::UnicodeEncoding::Utf8,
L"application/json"
};
// Send the request and get the response as a json value
const auto cloudShell = _SendAuthenticatedRequestReturningJson(cloudShellClient, shellRequest);
const auto cloudShell = _SendRequestReturningJson(uri, content, WWH::HttpMethod::Put());
// Return the uri
return cloudShell.at(L"properties").at(L"uri").as_string() + L"/";
return winrt::hstring{ std::wstring{ cloudShell.GetNamedObject(L"properties").GetNamedString(L"uri") } + L"/" };
}
// Method description:
// - helper function to request for a terminal
// Return value:
// - the uri for the terminal
utility::string_t AzureConnection::_GetTerminal(utility::string_t shellType)
winrt::hstring AzureConnection::_GetTerminal(const winrt::hstring& shellType)
{
// Initialize client
http_client terminalClient(_cloudShellUri);
auto uri{ fmt::format(L"{}terminals?cols={}&rows={}&version=2019-01-01&shell={}", _cloudShellUri, _initialCols, _initialRows, shellType) };
// Initialize the request
http_request terminalRequest(L"POST");
terminalRequest.set_request_uri(fmt::format(L"terminals?cols={}&rows={}&version=2019-01-01&shell={}", _initialCols, _initialRows, shellType));
// LOAD-BEARING. the API returns "'content-type' should be 'application/json' or 'multipart/form-data'"
terminalRequest.set_body(json::value::null());
WWH::HttpStringContent content{
L"",
WSS::UnicodeEncoding::Utf8,
// LOAD-BEARING. the API returns "'content-type' should be 'application/json' or 'multipart/form-data'"
L"application/json"
};
// Send the request and get the response as a json value
const auto terminalResponse = _SendAuthenticatedRequestReturningJson(terminalClient, terminalRequest);
_terminalID = terminalResponse.at(L"id").as_string();
const auto terminalResponse = _SendRequestReturningJson(uri, content);
_terminalID = terminalResponse.GetNamedString(L"id");
// Return the uri
return terminalResponse.at(L"socketUri").as_string();
return terminalResponse.GetNamedString(L"socketUri");
}
// Method description:
@ -972,16 +992,17 @@ namespace winrt::Microsoft::Terminal::TerminalConnection::implementation
// - we store the display name, tenant ID, access/refresh tokens, and token expiry
void AzureConnection::_StoreCredential()
{
json::value userName;
userName[U("ver")] = CurrentCredentialVersion;
WDJ::JsonObject userName;
userName.SetNamedValue(L"ver", WDJ::JsonValue::CreateNumberValue(CurrentCredentialVersion));
_packTenant(userName, *_currentTenant);
json::value passWord;
passWord[U("accessToken")] = json::value::string(_accessToken);
passWord[U("refreshToken")] = json::value::string(_refreshToken);
passWord[U("expiry")] = json::value::string(std::to_wstring(_expiry));
WDJ::JsonObject passWord;
passWord.SetNamedValue(L"accessToken", WDJ::JsonValue::CreateStringValue(_accessToken));
passWord.SetNamedValue(L"refreshToken", WDJ::JsonValue::CreateStringValue(_refreshToken));
passWord.SetNamedValue(L"expiry", WDJ::JsonValue::CreateStringValue(std::to_wstring(_expiry)));
PasswordVault vault;
PasswordCredential newCredential{ PasswordVaultResourceName, userName.serialize(), passWord.serialize() };
PasswordCredential newCredential{ PasswordVaultResourceName, userName.Stringify(), passWord.Stringify() };
vault.Add(newCredential);
}

Просмотреть файл

@ -5,9 +5,6 @@
#include "AzureConnection.g.h"
#include <cpprest/http_client.h>
#include <cpprest/http_listener.h>
#include <cpprest/ws_client.h>
#include <mutex>
#include <condition_variable>
@ -56,30 +53,30 @@ namespace winrt::Microsoft::Terminal::TerminalConnection::implementation
void _RunStoreState();
void _RunConnectState();
const utility::string_t _loginUri{ U("https://login.microsoftonline.com/") };
const utility::string_t _resourceUri{ U("https://management.azure.com/") };
const utility::string_t _wantedResource{ U("https://management.core.windows.net/") };
static constexpr std::wstring_view _loginUri{ L"https://login.microsoftonline.com/" };
static constexpr std::wstring_view _resourceUri{ L"https://management.azure.com/" };
static constexpr std::wstring_view _wantedResource{ L"https://management.core.windows.net/" };
const int _expireLimit{ 2700 };
utility::string_t _accessToken;
utility::string_t _refreshToken;
winrt::hstring _accessToken;
winrt::hstring _refreshToken;
int _expiry{ 0 };
utility::string_t _cloudShellUri;
utility::string_t _terminalID;
winrt::hstring _cloudShellUri;
winrt::hstring _terminalID;
std::vector<::Microsoft::Terminal::Azure::Tenant> _tenantList;
std::optional<::Microsoft::Terminal::Azure::Tenant> _currentTenant;
void _WriteStringWithNewline(const std::wstring_view str);
void _WriteCaughtExceptionRecord();
web::json::value _SendRequestReturningJson(web::http::client::http_client& theClient, web::http::http_request theRequest);
web::json::value _SendAuthenticatedRequestReturningJson(web::http::client::http_client& theClient, web::http::http_request theRequest);
web::json::value _GetDeviceCode();
web::json::value _WaitForUser(utility::string_t deviceCode, int pollInterval, int expiresIn);
winrt::Windows::Data::Json::JsonObject _SendRequestReturningJson(std::wstring_view uri, const winrt::Windows::Web::Http::IHttpContent& content = nullptr, winrt::Windows::Web::Http::HttpMethod method = nullptr);
void _setAccessToken(std::wstring_view accessToken);
winrt::Windows::Data::Json::JsonObject _GetDeviceCode();
winrt::Windows::Data::Json::JsonObject _WaitForUser(const winrt::hstring& deviceCode, int pollInterval, int expiresIn);
void _PopulateTenantList();
void _RefreshTokens();
web::json::value _GetCloudShellUserSettings();
utility::string_t _GetCloudShell();
utility::string_t _GetTerminal(utility::string_t shellType);
winrt::Windows::Data::Json::JsonObject _GetCloudShellUserSettings();
winrt::hstring _GetCloudShell();
winrt::hstring _GetTerminal(const winrt::hstring& shellType);
void _StoreCredential();
void _RemoveCredentials();
@ -95,9 +92,16 @@ namespace winrt::Microsoft::Terminal::TerminalConnection::implementation
std::optional<std::wstring> _ReadUserInput(InputMode mode);
web::websockets::client::websocket_client _cloudShellSocket;
winrt::Windows::Web::Http::HttpClient _httpClient{ nullptr };
wil::unique_winhttp_hinternet _socketSessionHandle;
wil::unique_winhttp_hinternet _socketConnectionHandle;
wil::unique_winhttp_hinternet _webSocket;
static std::optional<utility::string_t> _ParsePreferredShellType(const web::json::value& settingsResponse);
til::u8state _u8State{};
std::wstring _u16Str;
std::array<char, 4096> _buffer{};
static winrt::hstring _ParsePreferredShellType(const winrt::Windows::Data::Json::JsonObject& settingsResponse);
};
}

Просмотреть файл

@ -11,7 +11,6 @@
</PropertyGroup>
<PropertyGroup Label="NuGet Dependencies">
<TerminalCppWinrt>true</TerminalCppWinrt>
<TerminalCppRestSDK>true</TerminalCppRestSDK>
</PropertyGroup>
<Import Project="..\..\..\common.openconsole.props" Condition="'$(OpenConsoleDir)'==''" />
<Import Project="$(OpenConsoleDir)src\common.nugetversions.props" />
@ -99,39 +98,4 @@
<!-- This -must- go after cppwinrt.build.post.props because that includes many VS-provided props including appcontainer.common.props, which stomps on what cppwinrt.targets did. -->
<Import Project="$(OpenConsoleDir)src\common.nugetversions.targets" />
<!--
BODGY:
We depend on `cpprest142*.dll`, which comes from our vcpkg dependency. As a
part of the vcpkg dependency restore, msbuild will call the `deployBinary()`
function in
`packages\vcpkg-cpprestsdk.2.10.14\scripts\BuildSystems\msbuild\AppLocal.ps1`.
That function does the actual job of copying the file. It copies it outside of
MsBuild. MsBuild then, in the `_CopyFilesMarkedCopyLocal` target, determines
that it needs to copy `cpprest142*.dll`, because that dll is a member of
`@(ReferencesCopiedInThisBuild)`. However, the file's already been copied, so
MsBuild never copies it. But that also prevents MsBuild from setting
`WroteAtLeastOneFile`, which then means that MsBuild will never create the
.CopyComplete file for this project.
Because that file is missing, MsBuild will never think the project is up to
date, and the FastUpToDate check in VS will always force MsBuild to run a pass
on this project.
To mitigate this, we're adding this other target here, which runs after
_CopyFilesMarkedCopyLocal, and always creates the CopyUpToDateMarker. This
makes the FastUpToDate check succeed.
-->
<Target
Name="_Post_CopyFilesMarkedCopyLocal"
AfterTargets="_CopyFilesMarkedCopyLocal"
Condition="'@(ReferenceCopyLocalPaths)' != ''">
<Touch Files="@(CopyUpToDateMarker)"
AlwaysCreate="true" />
</Target>
</Project>

Просмотреть файл

@ -24,8 +24,14 @@
#include "winrt/Windows.Security.Credentials.h"
#include "winrt/Windows.Foundation.Collections.h"
#include "winrt/Windows.Web.Http.h"
#include "winrt/Windows.Web.Http.Headers.h"
#include "winrt/Windows.Data.Json.h"
#include <Windows.h>
#include <winhttp.h>
#include <wil/resource.h>
#include <TraceLoggingProvider.h>
TRACELOGGING_DECLARE_PROVIDER(g_hTerminalConnectionProvider);
#include <telemetry/ProjectTelemetry.h>

Просмотреть файл

@ -43,9 +43,6 @@
<!-- TAEF -->
<Import Project="$(MSBuildThisFileDirectory)..\packages\Microsoft.Taef.10.60.210621002\build\Microsoft.Taef.targets" Condition="'$(TerminalTAEF)' == 'true' and Exists('$(MSBuildThisFileDirectory)..\packages\Microsoft.Taef.10.60.210621002\build\Microsoft.Taef.targets')" />
<!-- CppRestSDK -->
<Import Project="$(MSBuildThisFileDirectory)..\packages\vcpkg-cpprestsdk.2.10.14\build\native\vcpkg-cpprestsdk.targets" Condition="'$(TerminalCppRestSDK)' == 'true' and Exists('$(MSBuildThisFileDirectory)..\packages\vcpkg-cpprestsdk.2.10.14\build\native\vcpkg-cpprestsdk.targets')" />
<!-- VCRTForwarders -->
<Import Project="$(MSBuildThisFileDirectory)..\packages\Microsoft.VCRTForwarders.140.1.0.4\build\native\Microsoft.VCRTForwarders.140.targets" Condition="'$(TerminalVCRTForwarders)' == 'true' and Exists('$(MSBuildThisFileDirectory)..\packages\Microsoft.VCRTForwarders.140.1.0.4\build\native\Microsoft.VCRTForwarders.140.targets')" />
@ -79,9 +76,6 @@
<!-- TAEF -->
<Error Condition="'$(TerminalTAEF)' == 'true' AND !Exists('$(MSBuildThisFileDirectory)..\packages\Microsoft.Taef.10.60.210621002\build\Microsoft.Taef.targets')" Text="$([System.String]::Format('$(ErrorText)', '$(MSBuildThisFileDirectory)..\packages\Microsoft.Taef.10.60.210621002\build\Microsoft.Taef.targets'))" />
<!-- CppRestSDK -->
<Error Condition="'$(TerminalCppRestSDK)' == 'true' AND !Exists('$(MSBuildThisFileDirectory)..\packages\vcpkg-cpprestsdk.2.10.14\build\native\vcpkg-cpprestsdk.targets')" Text="$([System.String]::Format('$(ErrorText)', '$(MSBuildThisFileDirectory)..\packages\vcpkg-cpprestsdk.2.10.14\build\native\vcpkg-cpprestsdk.targets'))" />
<!-- VCRTForwarders -->
<Error Condition="'$(TerminalVCRTForwarders)' == 'true' AND !Exists('$(MSBuildThisFileDirectory)..\packages\Microsoft.VCRTForwarders.140.1.0.4\build\native\Microsoft.VCRTForwarders.140.targets')" Text="$([System.String]::Format('$(ErrorText)', '$(MSBuildThisFileDirectory)..\packages\Microsoft.VCRTForwarders.140.1.0.4\build\native\Microsoft.VCRTForwarders.140.targets'))" />

Просмотреть файл

@ -145,4 +145,14 @@
<stage>AlwaysDisabled</stage>
</feature>
<feature>
<name>Feature_AzureConnectionInProc</name>
<description>Host the AzureConnection inside Terminal rather than via TerminalAzBridge</description>
<id>4661</id>
<stage>AlwaysDisabled</stage>
<alwaysEnabledBrandingTokens>
<brandingToken>Dev</brandingToken>
</alwaysEnabledBrandingTokens>
</feature>
</featureStaging>