Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

implement env vars in settings (#2785) #9116

Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
101 changes: 101 additions & 0 deletions src/cascadia/LocalTests_SettingsModel/ProfileTests.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,11 @@ namespace SettingsModelLocalTests
TEST_METHOD(LayerProfileProperties);
TEST_METHOD(LayerProfileIcon);
TEST_METHOD(LayerProfilesOnArray);
TEST_METHOD(ProfileWithEnvVarSelfRefKeyThrows);
TEST_METHOD(ProfileWithEnvVarCircularRefsThrows);
TEST_METHOD(ProfileWithEnvVarNoReferences);
TEST_METHOD(ProfileWithEnvVarProcessEnv);
TEST_METHOD(ProfileWithEnvVarComplex);

TEST_CLASS_SETUP(ClassSetup)
{
Expand Down Expand Up @@ -307,4 +312,100 @@ namespace SettingsModelLocalTests
VERIFY_ARE_EQUAL(L"profile4", settings->_allProfiles.GetAt(0).Name());
}

void ProfileTests::ProfileWithEnvVarSelfRefKeyThrows()
{
const std::string profileString{ R"({
"name": "profile0",
"guid": "{6239a42c-0000-49a3-80bd-e8fdd045185c}",
"environment": {
"VAR_1": "${env:VAR_1}"
}
})" };
VERIFY_THROWS(implementation::Profile::FromJson(VerifyParseSucceeded(profileString)), winrt::hresult_error);
}

void ProfileTests::ProfileWithEnvVarCircularRefsThrows()
{
const std::string profileString{ R"({
"name": "profile0",
"guid": "{6239a42c-0000-49a3-80bd-e8fdd045185c}",
"environment": {
"VAR_1": "${env:VAR_2}",
"VAR_2": "${env:VAR_3}",
"VAR_3": "${env:VAR_1}"
}
})" };
VERIFY_THROWS(implementation::Profile::FromJson(VerifyParseSucceeded(profileString)), winrt::hresult_error);
}

void ProfileTests::ProfileWithEnvVarNoReferences()
{
const std::string profileString{ R"({
"name": "profile0",
"guid": "{6239a42c-0000-49a3-80bd-e8fdd045185c}",
"environment": {
"VAR_1": "value1",
"VAR_2": "value2",
"VAR_3": "value3"
}
})" };
const auto profile = implementation::Profile::FromJson(VerifyParseSucceeded(profileString));
const auto envMap = profile->EnvironmentVariables();
VERIFY_ARE_EQUAL(static_cast<uint32_t>(3), envMap.Size());
VERIFY_ARE_EQUAL(L"value1", envMap.Lookup(L"VAR_1"));
VERIFY_ARE_EQUAL(L"value2", envMap.Lookup(L"VAR_2"));
VERIFY_ARE_EQUAL(L"value3", envMap.Lookup(L"VAR_3"));
}

void ProfileTests::ProfileWithEnvVarProcessEnv()
{
const std::string profileString{ R"({
"name": "profile0",
"guid": "{6239a42c-0000-49a3-80bd-e8fdd045185c}",
"environment": {
"VAR_1": "${env:SystemRoot}",
"VAR_2": "${env:___DOES_NOT_EXIST_IN_ENVIRONMENT___1234567890}"
}
})" };

std::wstring expectedSystemRoot(static_cast<size_t>(MAX_PATH), L'\0');
VERIFY_IS_TRUE(GetEnvironmentVariableW(L"SystemRoot", expectedSystemRoot.data(), MAX_PATH) > 0);
expectedSystemRoot.erase(std::find_if(expectedSystemRoot.rbegin(), expectedSystemRoot.rend(), [](wchar_t ch) {
return ch != L'\0';
}).base(),
expectedSystemRoot.end());
const auto profile = implementation::Profile::FromJson(VerifyParseSucceeded(profileString));
const auto envMap = profile->EnvironmentVariables();
VERIFY_ARE_EQUAL(static_cast<uint32_t>(2), envMap.Size());
VERIFY_ARE_EQUAL(expectedSystemRoot, envMap.Lookup(L"VAR_1"));
VERIFY_ARE_EQUAL(L"", envMap.Lookup(L"VAR_2"));
}

void ProfileTests::ProfileWithEnvVarComplex()
{
const std::string profileString{ R"({
"name": "profile0",
"guid": "{6239a42c-0000-49a3-80bd-e8fdd045185c}",
"environment": {
"TEST_HOME_DRIVE": "C:",
"TEST_HOME": "${env:TEST_HOME_DRIVE}${env:TEST_HOME_PATH}",
"TEST_HOME_PATH": "\\Users\\example",
"PARAM_START": "${env:TEST_HOME} abc",
"PARAM_MIDDLE": "abc ${env:TEST_HOME} abc",
"PARAM_END": "abc ${env:TEST_HOME}",
"PARAM_MULTIPLE": "${env:PARAM_START};${env:PARAM_MIDDLE};${env:PARAM_END}",
"PARAM_MULTIPLE_SAME": "${env:TEST_HOME_DRIVE};${env:TEST_HOME_DRIVE};${env:TEST_HOME_DRIVE};${env:TEST_HOME_DRIVE};${env:TEST_HOME_DRIVE};"
}
})" };

const auto profile = implementation::Profile::FromJson(VerifyParseSucceeded(profileString));
const auto envMap = profile->EnvironmentVariables();
VERIFY_ARE_EQUAL(static_cast<uint32_t>(8), envMap.Size());
VERIFY_ARE_EQUAL(L"C:\\Users\\example", envMap.Lookup(L"TEST_HOME"));
VERIFY_ARE_EQUAL(L"C:\\Users\\example abc", envMap.Lookup(L"PARAM_START"));
VERIFY_ARE_EQUAL(L"abc C:\\Users\\example abc", envMap.Lookup(L"PARAM_MIDDLE"));
VERIFY_ARE_EQUAL(L"abc C:\\Users\\example", envMap.Lookup(L"PARAM_END"));
VERIFY_ARE_EQUAL(L"C:\\Users\\example abc;abc C:\\Users\\example abc;abc C:\\Users\\example", envMap.Lookup(L"PARAM_MULTIPLE"));
VERIFY_ARE_EQUAL(L"C:;C:;C:;C:;C:;", envMap.Lookup(L"PARAM_MULTIPLE_SAME"));
}
}
2 changes: 1 addition & 1 deletion src/cascadia/TerminalApp/TerminalPage.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -907,7 +907,7 @@ namespace winrt::TerminalApp::implementation
{
std::wstring guidWString = Utils::GuidToString(profileGuid);

StringMap envMap{};
auto envMap = settings.EnvironmentVariables();
envMap.Insert(L"WT_PROFILE_ID", guidWString);
envMap.Insert(L"WSLENV", L"WT_PROFILE_ID");

Expand Down
5 changes: 5 additions & 0 deletions src/cascadia/TerminalApp/TerminalSettings.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -193,6 +193,11 @@ namespace winrt::TerminalApp::implementation
const til::color colorRef{ profile.TabColor().Value() };
_TabColor = static_cast<uint32_t>(colorRef);
}

if (profile.HasEnvironmentVariables())
{
_EnvironmentVariables = profile.EnvironmentVariables();
}
}

// Method Description:
Expand Down
5 changes: 4 additions & 1 deletion src/cascadia/TerminalApp/TerminalSettings.h
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,9 @@ Author(s):
#include "../inc/cppwinrt_utils.h"
#include <DefaultSettings.h>
#include <conattrs.hpp>
#include <winrt/impl/Windows.Foundation.Collections.2.h>

using namespace winrt::Windows::Foundation::Collections;
Comment on lines +22 to +24
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same deal in this header


// fwdecl unittest classes
namespace TerminalAppLocalTests
Expand Down Expand Up @@ -108,7 +111,7 @@ namespace winrt::TerminalApp::implementation
GETSET_SETTING(hstring, StartingDirectory);
GETSET_SETTING(hstring, StartingTitle);
GETSET_SETTING(bool, SuppressApplicationTitle);
GETSET_SETTING(hstring, EnvironmentVariables);
GETSET_SETTING(StringMap, EnvironmentVariables);

GETSET_SETTING(Microsoft::Terminal::TerminalControl::ScrollbarState, ScrollState, Microsoft::Terminal::TerminalControl::ScrollbarState::Visible);

Expand Down
2 changes: 1 addition & 1 deletion src/cascadia/TerminalControl/IControlSettings.idl
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ namespace Microsoft.Terminal.TerminalControl

String Commandline;
String StartingDirectory;
String EnvironmentVariables;
Windows.Foundation.Collections.StringMap EnvironmentVariables;

String BackgroundImage;
Double BackgroundImageOpacity;
Expand Down
1 change: 1 addition & 0 deletions src/cascadia/TerminalSettingsEditor/Profiles.h
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,7 @@ namespace winrt::Microsoft::Terminal::Settings::Editor::implementation
OBSERVABLE_PROJECTED_SETTING(_profile, CursorShape);
OBSERVABLE_PROJECTED_SETTING(_profile, CursorHeight);
OBSERVABLE_PROJECTED_SETTING(_profile, BellStyle);
OBSERVABLE_PROJECTED_SETTING(_profile, EnvironmentVariables);

private:
Model::Profile _profile;
Expand Down
1 change: 1 addition & 0 deletions src/cascadia/TerminalSettingsEditor/Profiles.idl
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@ namespace Microsoft.Terminal.Settings.Editor
OBSERVABLE_PROJECTED_SETTING(Microsoft.Terminal.TerminalControl.CursorStyle, CursorShape);
OBSERVABLE_PROJECTED_SETTING(UInt32, CursorHeight);
OBSERVABLE_PROJECTED_SETTING(Microsoft.Terminal.Settings.Model.BellStyle, BellStyle);
OBSERVABLE_PROJECTED_SETTING(Windows.Foundation.Collections.StringMap, EnvironmentVariables);
}

runtimeclass DeleteProfileEventArgs
Expand Down
90 changes: 90 additions & 0 deletions src/cascadia/TerminalSettingsModel/Profile.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@ static constexpr std::string_view AntialiasingModeKey{ "antialiasingMode" };
static constexpr std::string_view TabColorKey{ "tabColor" };
static constexpr std::string_view BellStyleKey{ "bellStyle" };
static constexpr std::string_view PixelShaderPathKey{ "experimental.pixelShaderPath" };
static constexpr std::string_view EnvironmentKey{ "environment" };

static constexpr std::wstring_view DesktopWallpaperEnum{ L"desktopWallpaper" };

Expand Down Expand Up @@ -275,6 +276,75 @@ bool Profile::ShouldBeLayered(const Json::Value& json) const
return sourceMatches;
}

namespace
{
Comment on lines +279 to +280
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: I don't think we need an extra namespace here. Declaring the function as static will keep it internal to this file, and that should be good enough

std::wstring ResolveEnvironmentVariableValue(const std::wstring& key, StringMap& envMap, std::map<std::wstring, bool> seenKeys)
{
if (envMap.HasKey(key))
{
if (seenKeys.find(key) != seenKeys.end())
{
std::wstringstream error;
error << L"Self referencing keys in environment settings: ";
for (auto it : seenKeys)
{
error << L"'" << it.first << L"', ";
}
error << L"'" << key << L"'";
throw winrt::hresult_error(WEB_E_INVALID_JSON_STRING, winrt::hstring(error.str()));
}
const std::wregex parameterRegex(L"\\$\\{env:(.*?)\\}");
std::wstring value(envMap.Lookup(key)), resolvedValue;
auto textIter = value.cbegin();
std::wsmatch regexMatch;
while (std::regex_search(textIter, value.cend(), regexMatch, parameterRegex))
{
if (regexMatch.size() != 2)
{
std::wstringstream error;
error << L"Unexpected regex match count '" << regexMatch.size()
<< L"' (expected '2') when matching '" << std::wstring(textIter, value.cend()) << L"'";
throw winrt::hresult_error(WEB_E_INVALID_JSON_STRING, winrt::hstring(error.str()));
}
resolvedValue += std::wstring(textIter, regexMatch[0].first);
const auto referencedKey = regexMatch[1];
if (referencedKey == key)
{
std::wstringstream error;
error << L"Self referencing key '" << key << L"' in environment settings";
throw winrt::hresult_error(WEB_E_INVALID_JSON_STRING, winrt::hstring(error.str()));
}
seenKeys[key] = true;
resolvedValue += ResolveEnvironmentVariableValue(referencedKey, envMap, seenKeys);
textIter = regexMatch[0].second;
}
std::copy(textIter, value.cend(), std::back_inserter(resolvedValue));

return resolvedValue;
}
const auto size = GetEnvironmentVariableW(key.c_str(), nullptr, 0);
if (size > 0)
{
std::wstring value(static_cast<size_t>(size), L'\0');
if (GetEnvironmentVariableW(key.c_str(), value.data(), size) > 0)
{
// Documentation (https://docs.microsoft.com/en-us/windows/win32/api/processenv/nf-processenv-getenvironmentvariablew)
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Surely, I've missed something here?

// says: "If the function succeeds, the return value is the number of characters stored in the buffer pointed to by lpBuffer,
// not including the terminating null character."
// However, my SystemDrive "C:" returns 3 and so without trimming we will end up with a stray NULL string terminator
// in the string.
value.erase(std::find_if(value.rbegin(), value.rend(), [](wchar_t ch) {
return ch != L'\0';
}).base(),
value.end());

return value;
}
}
return {};
}
}

// Method Description:
// - Layer values from the given json object on top of the existing properties
// of this object. For any keys we're expecting to be able to parse in the
Expand Down Expand Up @@ -339,6 +409,26 @@ void Profile::LayerJson(const Json::Value& json)
JsonUtils::GetValueForKey(json, TabColorKey, _TabColor);
JsonUtils::GetValueForKey(json, BellStyleKey, _BellStyle);
JsonUtils::GetValueForKey(json, PixelShaderPathKey, _PixelShaderPath);

const auto environmentKey = std::string(EnvironmentKey);
if (json.isMember(environmentKey))
{
auto environmentNode = json[environmentKey];
StringMap rawEnvMap{}, resolvedEnvMap{};
for (Json::Value::const_iterator it = environmentNode.begin(); it != environmentNode.end(); ++it)
{
rawEnvMap.Insert(JsonUtils::GetValue<hstring>(it.key()), JsonUtils::GetValue<hstring>(*it));
}

auto view = rawEnvMap.GetView();
for (auto it = view.First(); it.HasCurrent(); it.MoveNext())
{
const std::wstring key((*it).Key());
resolvedEnvMap.Insert(key, ResolveEnvironmentVariableValue(key, rawEnvMap, {}));
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since this last parameter is always {}, and we're never using it after the function is called, should we just remove it from the function signature and declare it internal to the function above?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's used to detect self or circular references when resolving the environment variables so it needs to be updated and passed in as a parameter (not reference). I can default it in the function declaration so it is not needed to be specified here if that helps?

}

EnvironmentVariables(resolvedEnvMap);
}
}

// Method Description:
Expand Down
5 changes: 5 additions & 0 deletions src/cascadia/TerminalSettingsModel/Profile.h
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,9 @@ Author(s):
#include "../inc/cppwinrt_utils.h"
#include "JsonUtils.h"
#include <DefaultSettings.h>
#include <winrt/impl/Windows.Foundation.Collections.2.h>
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You shouldn't need to include this header directly here - we should be able to get winrt::Windows::Foundation::Collections::StringMap from the Windows.Foundation.Collections.h in pch.h


using namespace winrt::Windows::Foundation::Collections;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this is gonna sound like a nit, but it's usually best practice to avoid using namespace ... statements in headers. It results in everyone who includes that header automatically using that namespace, which can be probelmatic. I know it's annoying that you need to have the whole namespaced name in the property declaration, but ¯\_(ツ)_/¯


// fwdecl unittest classes
namespace SettingsModelLocalTests
Expand Down Expand Up @@ -114,6 +117,8 @@ namespace winrt::Microsoft::Terminal::Settings::Model::implementation

GETSET_SETTING(Model::BellStyle, BellStyle, BellStyle::Audible);

GETSET_SETTING(StringMap, EnvironmentVariables);

private:
static std::wstring EvaluateStartingDirectory(const std::wstring& directory);

Expand Down
4 changes: 4 additions & 0 deletions src/cascadia/TerminalSettingsModel/Profile.idl
Original file line number Diff line number Diff line change
Expand Up @@ -190,5 +190,9 @@ namespace Microsoft.Terminal.Settings.Model
Boolean HasBellStyle();
void ClearBellStyle();
BellStyle BellStyle;

Boolean HasEnvironmentVariables();
void ClearEnvironmentVariables();
Windows.Foundation.Collections.StringMap EnvironmentVariables;
}
}