Compare commits

...

83 commits

Author SHA1 Message Date
Mike Griese 3b8e7236ae bunch of dead ends in this 2021-09-30 16:00:37 -05:00
Mike Griese f49c3fca01 Merge branch 'dev/migrie/f/just-elevated-state-2' into dev/migrie/f/non-terminal-content-elevation-warning 2021-09-30 12:49:03 -05:00
Mike Griese ae99ce9c36 teh 2021-09-30 12:48:09 -05:00
Mike Griese 0e7217d354 Merge commit 'aea37520b3cdb1c1752a6c8e0ff598991518ce28' into dev/migrie/f/just-elevated-state-2 2021-09-30 12:47:33 -05:00
Mike Griese aea37520b3 we want this 2021-09-30 12:47:03 -05:00
Mike Griese 3c1866ac53 Merge remote-tracking branch 'origin/dev/migrie/b/1.12-crash-on-exit' into dev/migrie/f/investigate-crashing 2021-09-30 11:28:32 -05:00
Mike Griese 48b20de4f4 Add some logging. Can't seem to get the crash to repro? 2021-09-30 11:15:35 -05:00
Mike Griese 9ff2775122 Merge branch 'dev/migrie/f/just-trying-something-on-just-elevated-state-2' into dev/migrie/f/just-elevated-state-2 2021-09-30 10:58:04 -05:00
Mike Griese b4e0496eff cleanup 2021-09-30 10:27:57 -05:00
Mike Griese ff333870fc Merge branch 'dev/migrie/f/just-elevated-state-2' into dev/migrie/f/non-terminal-content-elevation-warning 2021-09-30 10:06:23 -05:00
Mike Griese 866832b665 This seemingly works the way I'd expect, going to merge into the warning dialog and check 2021-09-30 09:39:17 -05:00
Mike Griese 56992296bf
Apply suggestions from code review
Co-authored-by: Carlos Zamora <carlos.zamora@microsoft.com>
2021-09-30 08:25:16 -05:00
Mike Griese d053f6cc9e Merge remote-tracking branch 'origin/main' into dev/migrie/f/just-elevated-state-2 2021-09-30 07:45:23 -05:00
Mike Griese a751156fcc Merge branch 'dev/migrie/f/just-elevated-state-2' into dev/migrie/f/non-terminal-content-elevation-warning 2021-09-28 10:53:35 -05:00
Mike Griese 620ee302fd Merge remote-tracking branch 'origin/main' into dev/migrie/f/just-elevated-state-2
Tossed out the AppState changes from #11083, since we're planning on just
  moving that to the Monarch anyways.
2021-09-28 10:36:29 -05:00
Mike Griese abb847b4c7 pr nits from miniksa 2021-09-27 10:29:38 -05:00
Mike Griese a3ac32ab25 derp 2021-09-22 15:57:44 -05:00
Mike Griese 5e9d0b8195 use the background from the control when we can 2021-09-22 15:49:41 -05:00
Mike Griese 62fe8235dc Merge branch 'dev/migrie/f/just-elevated-state-2' into dev/migrie/f/non-terminal-content-elevation-warning 2021-09-22 14:50:12 -05:00
Mike Griese 5ff9a247d4 okay so I guess that's not a word 2021-09-22 14:49:46 -05:00
Mike Griese 7e2b371dae Merge branch 'dev/migrie/f/just-elevated-state-2' into dev/migrie/f/non-terminal-content-elevation-warning 2021-09-22 14:48:29 -05:00
Mike Griese 02e9871f2a Merge remote-tracking branch 'origin/main' into dev/migrie/f/just-elevated-state-2 2021-09-22 14:46:25 -05:00
Mike Griese eee657b502 fix hot reloading for this file 2021-09-22 14:37:33 -05:00
Mike Griese a6e044d91c pr nits 2021-09-22 14:37:32 -05:00
Mike Griese f3738f5c1b Move window state, approvedCommandlines into user-state.json
This fixes the issue I had in the last commit. It's a little weird, but gets
  rid of the muckiness of layering. Things that are local to one elevation level
  won't pollute the other, and we don't need to worry about layering or where
  they came from. Just write shared state to `state.json`, and window state to
  `elevated-state`/`user-state`
2021-09-22 14:37:32 -05:00
Mike Griese 7e2e4eaf49 I think this works ALMOST as I want
* [x] delete all state, open terminal elevated - generated profiles go to the correct place, approved commandlines go to the elevated one.
  * [x] delete some dynamic profiles, open the terminal - they don't resurrect in either IL
  * [ ] GAH saving a window layout unelevated, then trying to save one elevated will blow away the unelevated one
2021-09-22 14:37:25 -05:00
Mike Griese 8635537ebc fix hot reloading for this file 2021-09-22 14:35:02 -05:00
Mike Griese de9dc32aa0 pr nits 2021-09-22 12:26:33 -05:00
Mike Griese 64d02f2b2b Move window state, approvedCommandlines into user-state.json
This fixes the issue I had in the last commit. It's a little weird, but gets
  rid of the muckiness of layering. Things that are local to one elevation level
  won't pollute the other, and we don't need to worry about layering or where
  they came from. Just write shared state to `state.json`, and window state to
  `elevated-state`/`user-state`
2021-09-22 12:07:34 -05:00
Mike Griese 94c4cca176 I think this works ALMOST as I want
* [x] delete all state, open terminal elevated - generated profiles go to the correct place, approved commandlines go to the elevated one.
  * [x] delete some dynamic profiles, open the terminal - they don't resurrect in either IL
  * [ ] GAH saving a window layout unelevated, then trying to save one elevated will blow away the unelevated one
2021-09-22 11:23:33 -05:00
Mike Griese b2796019f8 Merge branch 'dev/migrie/f/just-elevated-state-2' into dev/migrie/f/non-terminal-content-elevation-warning 2021-09-22 10:03:45 -05:00
Mike Griese b755eb0f20 Merge remote-tracking branch 'origin/main' into dev/migrie/f/non-terminal-content-elevation-warning 2021-09-22 10:03:37 -05:00
Mike Griese b1b1befeb9 this is a crazy idea that I hate but gotta start somewhere 2021-09-22 10:03:07 -05:00
Mike Griese 507a48ed68 Merge remote-tracking branch 'origin/main' into dev/migrie/f/just-elevated-state-2 2021-09-22 08:27:01 -05:00
Mike Griese 5a8e27e60a Merge branch 'dev/migrie/f/just-elevated-state-2' into dev/migrie/f/non-terminal-content-elevation-warning 2021-09-20 12:42:59 -05:00
Mike Griese 9b3b9e0109 remove baseapplicationstate and just merge it back in 2021-09-20 12:42:41 -05:00
Mike Griese 7f9f75cab3 gah, I might have broke persisting window state 2021-09-20 12:11:30 -05:00
Mike Griese d0f05f60e9 Merge branch 'dev/migrie/f/just-elevated-state-2' into dev/migrie/f/non-terminal-content-elevation-warning 2021-09-20 11:38:19 -05:00
Mike Griese 56850639c5 Merge remote-tracking branch 'origin/main' into dev/migrie/f/just-elevated-state-2 2021-09-20 11:34:59 -05:00
Mike Griese a4acdeb5f2 blindly remove ElevatedState 2021-09-20 11:34:11 -05:00
Mike Griese 56d5f9ee3a every day I'm mangling 2021-09-14 16:44:38 -05:00
Mike Griese d944a68ded manually allow env var parsing. Remember that wsl needs manual allows too 2021-09-14 16:07:47 -05:00
Mike Griese 723037ef99 fix the focus thing, frick the env vars thing 2021-09-14 15:47:01 -05:00
Mike Griese 54a002762a comments 2021-09-14 15:36:03 -05:00
Mike Griese 47d55a8fd0 don't do the lookup for things in system32 2021-09-14 15:22:20 -05:00
Mike Griese edd71265d8 Merge branch 'dev/migrie/f/just-elevated-state-2' into dev/migrie/f/non-terminal-content-elevation-warning 2021-09-14 12:16:25 -05:00
Mike Griese 6757452d6d fix the tests 2021-09-14 12:14:50 -05:00
Mike Griese 4e69a32de7 bunch of new allowed words 2021-09-14 06:01:24 -05:00
Mike Griese da0cc7bae5 todo! in the code 2021-09-14 05:59:54 -05:00
Mike Griese 51e0473560 minor nits
(cherry picked from commit 306ad30753)
2021-09-14 05:49:10 -05:00
Mike Griese c106f64bc7 🤦
(cherry picked from commit 00c7647594)
2021-09-14 05:49:04 -05:00
Mike Griese 6be697221d This is everything from dev/migrie/f/non-terminal-content-elevation-warning for specifically ElevatedState
(cherry picked from commit 97b0f06504)
2021-09-14 05:48:56 -05:00
Mike Griese d6c2fb593d THis fixes #7754 2021-09-14 05:20:45 -05:00
Mike Griese 1e3a319314 more unique_sid's, and m,ore comments 2021-09-13 14:32:46 -05:00
Mike Griese 4da965f901 Merge remote-tracking branch 'origin/main' into dev/migrie/f/non-terminal-content-elevation-warning 2021-09-13 14:20:03 -05:00
Mike Griese 4f697eca92 I thing this was unneeded 2021-09-13 12:11:04 -05:00
Mike Griese 1111d41347 This works to do the 'blow the file away when permissions have changed' thing 2021-09-13 11:45:11 -05:00
Mike Griese 6265f4f1d7 this looks like it checks the permissions as we'd expect 2021-09-09 14:00:51 -05:00
Mike Griese 7854abe0a3 I bet that's what I was doing wrong. The rename is just dandy, it don't care about elevation or none of that, so the unelevated terminal could atomically rewrite the elevated file, which isn't what we wanted. 2021-09-09 12:09:06 -05:00
Mike Griese 9a1cf5ac6e bunch of dead ends 2021-09-09 12:03:42 -05:00
Mike Griese aef422d5f1 This is definitely not right.
Sublime can delete the file just fine (and write any old file in it's place)
  Even we can modify the file when running unelevated, which is **B**ad.

  So there's gotta be some way to allow _us_ to write the file only when elevated
2021-09-09 10:23:53 -05:00
Mike Griese 8569211d0f Now, we can update the file after it's created 2021-09-09 10:18:22 -05:00
Mike Griese 721b6367a2 unfortunately, we can't _write_ this file... 2021-09-09 10:11:52 -05:00
Mike Griese 5197dc4e50 this worked, nice 2021-09-09 08:56:53 -05:00
Mike Griese 880222dc1b This file isn't even readable by admins, so maybe that's bad 2021-09-09 08:23:59 -05:00
Mike Griese 447c2f9d4f I think this creates a test file that's only accessible by SYSTEM. Asked raymond chen for help. 2021-09-08 15:51:21 -05:00
Mike Griese b592155d2b Merge remote-tracking branch 'origin/main' into dev/migrie/f/non-terminal-content-elevation-warning 2021-09-08 11:50:56 -05:00
Mike Griese 64898b1caf update other panes too 2021-09-02 11:50:33 -05:00
Mike Griese e5dc64e085 Hot-reload the elevated state? 2021-09-02 11:12:07 -05:00
Mike Griese 58c6646132 Format that dialog with the actual commandline 2021-09-02 11:01:11 -05:00
Mike Griese 9b32681ae1 This works to prompt before splitting a pane 2021-09-02 10:09:22 -05:00
Mike Griese 7f29e1e268 More things we might need for Non-Terminal content, #997 2021-09-02 10:09:05 -05:00
Mike Griese 736a351c27 This works amazingly well 2021-09-01 17:08:00 -05:00
Mike Griese 631cdf7b18 Who ever said nested lambdas is a bad thing? 2021-09-01 16:42:36 -05:00
Mike Griese 7fb7d64b91 These are things I might need for #997 2021-09-01 16:38:58 -05:00
Mike Griese 1ee3522cd8 Allow a TerminalTab to have a UserControl 2021-09-01 15:27:30 -05:00
Mike Griese ed1cf2aeac add the boilerplate for a custom content dialog like thing 2021-09-01 14:57:41 -05:00
Mike Griese c66a56656e Allow a Pane to host a UserControl instead of a TermControl 2021-09-01 12:53:46 -05:00
Mike Griese 14d21f492b nits, cleanup 2021-09-01 12:52:45 -05:00
Mike Griese ccbcb425da Merge remote-tracking branch 'origin/main' into dev/migrie/f/elevation-warning 2021-08-31 16:16:02 -05:00
Mike Griese 2857324777 proof of concept add a dialog to pop when opening new tabs 2021-08-31 16:15:49 -05:00
Mike Griese eb243f5e11 Split ApplicationState into a Base and add an Elevated version 2021-08-31 16:15:22 -05:00
Mike Griese 42c3eea136 pull application state members into a base class 2021-08-30 12:47:10 -05:00
32 changed files with 1257 additions and 332 deletions

View file

@ -1,3 +1,4 @@
admins
apc
calt
ccmp

View file

@ -1,5 +1,7 @@
ACCEPTFILES
ACCESSDENIED
acl
aclapi
alignas
alignof
APPLYTOSUBMENUS
@ -19,6 +21,7 @@ comparand
cstdint
CXICON
CYICON
Dacl
dataobject
dcomp
DERR
@ -114,9 +117,12 @@ OSVERSIONINFOEXW
otms
OUTLINETEXTMETRICW
overridable
PACL
PAGESCROLL
PEXPLICIT
PICKFOLDERS
pmr
ptstr
rcx
REGCLS
RETURNCMD

View file

@ -61,6 +61,9 @@ Author(s):
// Manually include til after we include Windows.Foundation to give it winrt superpowers
#include "til.h"
#include <til/mutex.h>
#include <til/throttled_func.h>
// Common includes for most tests:
#include "../../inc/argb.h"
#include "../../inc/conattrs.hpp"

View file

@ -0,0 +1,88 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT license.
#pragma once
#include "pch.h"
#include "AdminWarningPlaceholder.h"
#include "AdminWarningPlaceholder.g.cpp"
#include <UIAutomationCore.h>
#include <LibraryResources.h>
using namespace winrt::Windows::UI::Xaml;
using namespace winrt::Windows::UI::Xaml::Automation::Peers;
namespace winrt::TerminalApp::implementation
{
AdminWarningPlaceholder::AdminWarningPlaceholder(const winrt::Microsoft::Terminal::Control::TermControl& control, const winrt::hstring& cmdline) :
_control{ control },
_Commandline{ cmdline }
{
InitializeComponent();
// If the content we're hosting is a TermControl, then use the control's
// BG as our BG, to give the impression that it's there, under the
// dialog.
if (const auto termControl{ control.try_as<winrt::Microsoft::Terminal::Control::TermControl>() })
{
RootGrid().Background(termControl.BackgroundBrush());
}
_layoutUpdatedRevoker = RootGrid().LayoutUpdated(winrt::auto_revoke, [this](auto /*s*/, auto /*e*/) {
// Only let this succeed once.
_layoutUpdatedRevoker.revoke();
if (auto automationPeer{ FrameworkElementAutomationPeer::FromElement(ApproveCommandlineWarningTitle()) })
{
// automationPeer.try_as<FrameworkElementAutomationPeer>().RaiseStructureChangedEvent(Automation::Peers::AutomationStructureChangeType::ChildrenBulkAdded, ApproveCommandlineWarningPrefixTextBlock());
automationPeer.RaiseNotificationEvent(
AutomationNotificationKind::ActionCompleted,
AutomationNotificationProcessing::CurrentThenMostRecent,
L"Foo",
L"ApproveCommandlineWarningTitle" /* unique name for this notification category */
);
}
CancelButton().Focus(FocusState::Programmatic);
});
}
void AdminWarningPlaceholder::_primaryButtonClick(winrt::Windows::Foundation::IInspectable const& /*sender*/,
RoutedEventArgs const& e)
{
if (auto automationPeer{ FrameworkElementAutomationPeer::FromElement(PrimaryButton()) })
{
automationPeer.RaiseNotificationEvent(
AutomationNotificationKind::ActionCompleted,
AutomationNotificationProcessing::CurrentThenMostRecent,
L"PrimaryButton",
L"_primaryButtonClick" /* unique name for this notification category */
);
}
_PrimaryButtonClickedHandlers(*this, e);
}
void AdminWarningPlaceholder::_cancelButtonClick(winrt::Windows::Foundation::IInspectable const& /*sender*/,
RoutedEventArgs const& e)
{
if (auto automationPeer{ FrameworkElementAutomationPeer::FromElement(CancelButton()) })
{
automationPeer.RaiseNotificationEvent(
AutomationNotificationKind::ActionCompleted,
AutomationNotificationProcessing::CurrentThenMostRecent,
L"CancelButton",
L"_cancelButtonClick" /* unique name for this notification category */
);
}
_CancelButtonClickedHandlers(*this, e);
}
winrt::Windows::UI::Xaml::Controls::UserControl AdminWarningPlaceholder::Control()
{
return _control;
}
winrt::hstring AdminWarningPlaceholder::ControlName() const
{
return RS_(L"AdminWarningPlaceholderControlName");
}
}

View file

@ -0,0 +1,33 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT license.
#pragma once
#include "AdminWarningPlaceholder.g.h"
#include "../../cascadia/inc/cppwinrt_utils.h"
namespace winrt::TerminalApp::implementation
{
struct AdminWarningPlaceholder : AdminWarningPlaceholderT<AdminWarningPlaceholder>
{
AdminWarningPlaceholder(const winrt::Microsoft::Terminal::Control::TermControl& control, const winrt::hstring& cmdline);
winrt::hstring ControlName() const;
winrt::Windows::UI::Xaml::Controls::UserControl Control();
WINRT_CALLBACK(PropertyChanged, Windows::UI::Xaml::Data::PropertyChangedEventHandler);
WINRT_OBSERVABLE_PROPERTY(winrt::hstring, Commandline, _PropertyChangedHandlers);
TYPED_EVENT(PrimaryButtonClicked, TerminalApp::AdminWarningPlaceholder, winrt::Windows::UI::Xaml::RoutedEventArgs);
TYPED_EVENT(CancelButtonClicked, TerminalApp::AdminWarningPlaceholder, winrt::Windows::UI::Xaml::RoutedEventArgs);
private:
friend struct AdminWarningPlaceholderT<AdminWarningPlaceholder>; // friend our parent so it can bind private event handlers
winrt::Microsoft::Terminal::Control::TermControl _control{ nullptr };
void _primaryButtonClick(winrt::Windows::Foundation::IInspectable const& sender,
winrt::Windows::UI::Xaml::RoutedEventArgs const& e);
void _cancelButtonClick(winrt::Windows::Foundation::IInspectable const& sender,
winrt::Windows::UI::Xaml::RoutedEventArgs const& e);
winrt::Windows::UI::Xaml::Controls::Grid::LayoutUpdated_revoker _layoutUpdatedRevoker;
};
}

View file

@ -0,0 +1,12 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT license.
namespace TerminalApp
{
[default_interface] runtimeclass AdminWarningPlaceholder : Windows.UI.Xaml.Controls.UserControl,
Windows.UI.Xaml.Data.INotifyPropertyChanged
{
String Commandline { get; };
String ControlName { get; };
}
}

View file

@ -0,0 +1,79 @@
<!--
Copyright (c) Microsoft Corporation. All rights reserved. Licensed under
the MIT License. See LICENSE in the project root for license information.
-->
<UserControl x:Class="TerminalApp.AdminWarningPlaceholder"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:local="using:TerminalApp"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:mux="using:Microsoft.UI.Xaml.Controls"
AutomationProperties.Name="{x:Bind ControlName, Mode=OneWay}"
IsTabStop="True"
mc:Ignorable="d">
<!--
We have to use two grids to be tricky here:
- The outer grid will get its background from the control it hosts, and
expands to fit its container. This will make it look like the background
fills the space available.
- The inner grid is set to Center,Center, so that the "dialog" appears
centered
-->
<Grid x:Name="RootGrid"
HorizontalAlignment="Stretch"
VerticalAlignment="Stretch">
<Grid HorizontalAlignment="Center"
VerticalAlignment="Center">
<Border Margin="8,8,8,8"
Padding="16,8,16,8"
Background="{ThemeResource SystemControlBackgroundAltHighBrush}"
BorderBrush="{ThemeResource SystemAccentColor}"
BorderThickness="2,2,2,2"
CornerRadius="{ThemeResource OverlayCornerRadius}">
<StackPanel Orientation="Vertical">
<TextBlock x:Uid="ApproveCommandlineWarningTitle"
x:Name="ApproveCommandlineWarningTitle"
Padding="0,0,0,16"
HorizontalAlignment="Left"
FontSize="20"
FontWeight="Normal"
TextWrapping="WrapWholeWords" />
<TextBlock x:Uid="ApproveCommandlineWarningPrefixTextBlock"
x:Name="ApproveCommandlineWarningPrefixTextBlock"
HorizontalAlignment="Left"
TextWrapping="WrapWholeWords" />
<TextBlock Margin="0,8,0,8"
HorizontalAlignment="Center"
FontFamily="Cascadia Code"
Text="{x:Bind Commandline, Mode=OneWay}"
TextWrapping="WrapWholeWords" />
<TextBlock x:Uid="ApproveCommandlineWarningSuffixTextBlock"
HorizontalAlignment="Left"
TextWrapping="WrapWholeWords" />
<StackPanel HorizontalAlignment="Right"
Orientation="Horizontal">
<Button x:Name="PrimaryButton"
x:Uid="ApproveCommandlineWarning_PrimaryButton"
Margin="8"
IsTabStop="True"
HorizontalAlignment="Right"
Click="_primaryButtonClick"
Style="{StaticResource AccentButtonStyle}" />
<Button x:Name="CancelButton"
x:Uid="ApproveCommandlineWarning_CancelButton"
IsTabStop="True"
HorizontalAlignment="Right"
Click="_cancelButtonClick" />
</StackPanel>
</StackPanel>
</Border>
</Grid>
</Grid>
</UserControl>

View file

@ -12,6 +12,8 @@
#include <WtExeUtils.h>
#include <wil/token_helpers.h >
#include "../../types/inc/utils.hpp"
using namespace winrt::Windows::ApplicationModel;
using namespace winrt::Windows::ApplicationModel::DataTransfer;
using namespace winrt::Windows::UI::Xaml;
@ -139,28 +141,8 @@ static Documents::Run _BuildErrorRun(const winrt::hstring& text, const ResourceD
// Return Value:
// - true if the user is an administrator
static bool _isUserAdmin() noexcept
try
{
wil::unique_handle processToken{ GetCurrentProcessToken() };
const auto elevationType = wil::get_token_information<TOKEN_ELEVATION_TYPE>(processToken.get());
const auto elevationState = wil::get_token_information<TOKEN_ELEVATION>(processToken.get());
if (elevationType == TokenElevationTypeDefault && elevationState.TokenIsElevated)
{
// In this case, the user has UAC entirely disabled. This is sort of
// weird, we treat this like the user isn't an admin at all. There's no
// separation of powers, so the things we normally want to gate on
// "having special powers" doesn't apply.
//
// See GH#7754, GH#11096
return false;
}
return wil::test_token_membership(nullptr, SECURITY_NT_AUTHORITY, SECURITY_BUILTIN_DOMAIN_RID, DOMAIN_ALIAS_RID_ADMINS);
}
catch (...)
{
LOG_CAUGHT_EXCEPTION();
return false;
return Microsoft::Console::Utils::IsElevated();
}
namespace winrt::TerminalApp::implementation
@ -895,8 +877,6 @@ namespace winrt::TerminalApp::implementation
void AppLogic::_RegisterSettingsChange()
{
const std::filesystem::path settingsPath{ std::wstring_view{ CascadiaSettings::SettingsPath() } };
const std::filesystem::path statePath{ std::wstring_view{ ApplicationState::SharedInstance().FilePath() } };
_reader.create(
settingsPath.parent_path().c_str(),
false,
@ -905,14 +885,20 @@ namespace winrt::TerminalApp::implementation
// editors, who will write a temp file, then rename it to be the
// actual file you wrote. So listen for that too.
wil::FolderChangeEvents::FileName | wil::FolderChangeEvents::LastWriteTime,
[this, settingsBasename = settingsPath.filename(), stateBasename = statePath.filename()](wil::FolderChangeEvent, PCWSTR fileModified) {
const auto modifiedBasename = std::filesystem::path{ fileModified }.filename();
[this, settingsBasename = settingsPath.filename()](wil::FolderChangeEvent, PCWSTR fileModified) {
// DO NOT create a static reference to
// ApplicationState::SharedInstance here. See
// https://github.com/microsoft/terminal/pull/11222/files/9ff2775122a496fb8b1bcc7a0b83a64ce5b26c5f#r719627541
// for why. ApplicationState::SharedInstance already caches it's
// own static ref.
const winrt::hstring modifiedBasename{ std::filesystem::path{ fileModified }.filename().c_str() };
if (modifiedBasename == settingsBasename)
{
_reloadSettings->Run();
}
else if (modifiedBasename == stateBasename)
else if (ApplicationState::SharedInstance().IsStatePath(modifiedBasename))
{
_reloadState();
}

View file

@ -34,7 +34,7 @@ static const Duration AnimationDuration = DurationHelper::FromTimeSpan(winrt::Wi
winrt::Windows::UI::Xaml::Media::SolidColorBrush Pane::s_focusedBorderBrush = { nullptr };
winrt::Windows::UI::Xaml::Media::SolidColorBrush Pane::s_unfocusedBorderBrush = { nullptr };
Pane::Pane(const Profile& profile, const TermControl& control, const bool lastFocused) :
Pane::Pane(const Profile& profile, const Controls::UserControl& control, const bool lastFocused) :
_control{ control },
_lastActive{ lastFocused },
_profile{ profile }
@ -42,8 +42,11 @@ Pane::Pane(const Profile& profile, const TermControl& control, const bool lastFo
_root.Children().Append(_borderFirst);
_borderFirst.Child(_control);
_connectionStateChangedToken = _control.ConnectionStateChanged({ this, &Pane::_ControlConnectionStateChangedHandler });
_warningBellToken = _control.WarningBell({ this, &Pane::_ControlWarningBellHandler });
if (const auto& termControl{ _control.try_as<TermControl>() })
{
_connectionStateChangedToken = termControl.ConnectionStateChanged({ this, &Pane::_ControlConnectionStateChangedHandler });
_warningBellToken = termControl.WarningBell({ this, &Pane::_ControlWarningBellHandler });
}
// On the first Pane's creation, lookup resources we'll use to theme the
// Pane, including the brushed to use for the focused/unfocused border
@ -125,7 +128,19 @@ NewTerminalArgs Pane::GetTerminalArgsForPane() const
assert(_IsLeaf());
NewTerminalArgs args{};
auto controlSettings = _control.Settings().as<TerminalSettings>();
auto termControl{ _control.try_as<TermControl>() };
if (!termControl)
{
if (auto adminWarning{ _control.try_as<AdminWarningPlaceholder>() })
{
termControl = adminWarning.Content().try_as<TermControl>();
}
}
if (!termControl)
{
return nullptr;
}
auto controlSettings = termControl.Settings().as<TerminalSettings>();
args.Profile(controlSettings.ProfileName());
args.StartingDirectory(controlSettings.StartingDirectory());
@ -824,6 +839,31 @@ bool Pane::SwapPanes(std::shared_ptr<Pane> first, std::shared_ptr<Pane> second)
return false;
}
winrt::Windows::UI::Xaml::Controls::UserControl Pane::ReplaceControl(const winrt::Windows::UI::Xaml::Controls::UserControl& control)
{
if (!_IsLeaf())
{
return nullptr;
}
const auto& oldControl = _control;
if (const auto& oldTermControl{ _control.try_as<TermControl>() })
{
oldTermControl.ConnectionStateChanged(_connectionStateChangedToken);
oldTermControl.WarningBell(_warningBellToken);
}
_control = control;
_borderFirst.Child(_control);
if (const auto& termControl{ _control.try_as<TermControl>() })
{
_connectionStateChangedToken = termControl.ConnectionStateChanged({ this, &Pane::_ControlConnectionStateChangedHandler });
_warningBellToken = termControl.WarningBell({ this, &Pane::_ControlWarningBellHandler });
}
return oldControl;
}
// Method Description:
// - Given two panes' offsets, test whether the `direction` side of first is adjacent to second.
// Arguments:
@ -1071,8 +1111,12 @@ void Pane::_ControlConnectionStateChangedHandler(const winrt::Windows::Foundatio
{
return;
}
const auto newConnectionState = _control.ConnectionState();
const auto& termControl{ _control.try_as<TermControl>() };
if (!termControl)
{
return;
}
const auto newConnectionState = termControl.ConnectionState();
const auto previousConnectionState = std::exchange(_connectionState, newConnectionState);
if (newConnectionState < ConnectionState::Closed)
@ -1115,7 +1159,9 @@ void Pane::_ControlWarningBellHandler(const winrt::Windows::Foundation::IInspect
{
return;
}
if (_profile)
const auto& termControl{ _control.try_as<TermControl>() };
if (_profile && termControl)
{
// We don't want to do anything if nothing is set, so check for that first
if (static_cast<int>(_profile.BellStyle()) != 0)
@ -1129,7 +1175,7 @@ void Pane::_ControlWarningBellHandler(const winrt::Windows::Foundation::IInspect
if (WI_IsFlagSet(_profile.BellStyle(), winrt::Microsoft::Terminal::Settings::Model::BellStyle::Window))
{
_control.BellLightOn();
termControl.BellLightOn();
}
// raise the event with the bool value corresponding to the taskbar flag
@ -1189,7 +1235,11 @@ void Pane::Shutdown()
std::unique_lock lock{ _createCloseLock };
if (_IsLeaf())
{
_control.Close();
const auto& termControl{ _control.try_as<TermControl>() };
if (termControl)
{
termControl.Close();
}
}
else
{
@ -1199,7 +1249,7 @@ void Pane::Shutdown()
}
// Method Description:
// - Get the root UIElement of this pane. There may be a single TermControl as a
// - Get the root UIElement of this pane. There may be a single UserControl as a
// child, or an entire tree of grids and panes as children of this element.
// Arguments:
// - <none>
@ -1258,7 +1308,7 @@ TermControl Pane::GetLastFocusedTerminalControl()
{
if (p->_IsLeaf())
{
return p->_control;
return p->GetTerminalControl();
}
pane = p;
}
@ -1266,7 +1316,7 @@ TermControl Pane::GetLastFocusedTerminalControl()
}
return _firstChild->GetLastFocusedTerminalControl();
}
return _control;
return GetTerminalControl();
}
// Method Description:
@ -1275,8 +1325,14 @@ TermControl Pane::GetLastFocusedTerminalControl()
// Arguments:
// - <none>
// Return Value:
// - nullptr if this Pane is a parent, otherwise the TermControl of this Pane.
TermControl Pane::GetTerminalControl()
// - nullptr if this Pane is a parent or isn't hosting a Terminal, otherwise the
// TermControl of this Pane.
TermControl Pane::GetTerminalControl() const
{
return _IsLeaf() ? _control.try_as<TermControl>() : nullptr;
}
Controls::UserControl Pane::GetUserControl() const
{
return _IsLeaf() ? _control : nullptr;
}
@ -1449,9 +1505,13 @@ void Pane::_FocusFirstChild()
void Pane::UpdateSettings(const TerminalSettingsCreateResult& settings, const Profile& profile)
{
assert(_IsLeaf());
const auto& termControl{ _control.try_as<TermControl>() };
if (!termControl)
{
return;
}
_profile = profile;
auto controlSettings = _control.Settings().as<TerminalSettings>();
auto controlSettings = termControl.Settings().as<TerminalSettings>();
// Update the parent of the control's settings object (and not the object itself) so
// that any overrides made by the control don't get affected by the reload
controlSettings.SetParent(settings.DefaultSettings());
@ -1464,8 +1524,8 @@ void Pane::UpdateSettings(const TerminalSettingsCreateResult& settings, const Pr
// sure the unfocused settings inherit from that.
unfocusedSettings.SetParent(controlSettings);
}
_control.UnfocusedAppearance(unfocusedSettings);
_control.UpdateSettings();
termControl.UnfocusedAppearance(unfocusedSettings);
termControl.UpdateSettings();
}
// Method Description:
@ -1606,8 +1666,12 @@ void Pane::_CloseChild(const bool closeFirst, const bool isDetaching)
_id = remainingChild->Id();
// Add our new event handler before revoking the old one.
_connectionStateChangedToken = _control.ConnectionStateChanged({ this, &Pane::_ControlConnectionStateChangedHandler });
_warningBellToken = _control.WarningBell({ this, &Pane::_ControlWarningBellHandler });
const auto& termControl{ _control.try_as<TermControl>() };
if (termControl)
{
_connectionStateChangedToken = termControl.ConnectionStateChanged({ this, &Pane::_ControlConnectionStateChangedHandler });
_warningBellToken = termControl.WarningBell({ this, &Pane::_ControlWarningBellHandler });
}
// Revoke the old event handlers. Remove both the handlers for the panes
// themselves closing, and remove their handlers for their controls
@ -1621,8 +1685,11 @@ void Pane::_CloseChild(const bool closeFirst, const bool isDetaching)
closedChild->WalkTree([](auto p) {
if (p->_IsLeaf())
{
p->_control.ConnectionStateChanged(p->_connectionStateChangedToken);
p->_control.WarningBell(p->_warningBellToken);
if (const auto& closedControl{ p->_control.try_as<TermControl>() })
{
closedControl.ConnectionStateChanged(p->_connectionStateChangedToken);
closedControl.WarningBell(p->_warningBellToken);
}
}
return false;
});
@ -1630,15 +1697,19 @@ void Pane::_CloseChild(const bool closeFirst, const bool isDetaching)
closedChild->Closed(closedChildClosedToken);
remainingChild->Closed(remainingChildClosedToken);
remainingChild->_control.ConnectionStateChanged(remainingChild->_connectionStateChangedToken);
remainingChild->_control.WarningBell(remainingChild->_warningBellToken);
if (const auto& remainingControl{ remainingChild->_control.try_as<TermControl>() })
{
remainingControl.ConnectionStateChanged(remainingChild->_connectionStateChangedToken);
remainingControl.WarningBell(remainingChild->_warningBellToken);
}
// If we or either of our children was focused, we want to take that
// focus from them.
_lastActive = _lastActive || _firstChild->_lastActive || _secondChild->_lastActive;
// Remove all the ui elements of the remaining child. This'll make sure
// we can re-attach the TermControl to our Grid.
// we can re-attach the UserControl to our Grid.
remainingChild->_root.Children().Clear();
remainingChild->_borderFirst.Child(nullptr);
@ -1649,7 +1720,7 @@ void Pane::_CloseChild(const bool closeFirst, const bool isDetaching)
_root.ColumnDefinitions().Clear();
_root.RowDefinitions().Clear();
// Reattach the TermControl to our grid.
// Reattach the UserControl to our grid.
_root.Children().Append(_borderFirst);
_borderFirst.Child(_control);
@ -1711,8 +1782,11 @@ void Pane::_CloseChild(const bool closeFirst, const bool isDetaching)
closedChild->WalkTree([](auto p) {
if (p->_IsLeaf())
{
p->_control.ConnectionStateChanged(p->_connectionStateChangedToken);
p->_control.WarningBell(p->_warningBellToken);
if (const auto& closedControl{ p->_control.try_as<TermControl>() })
{
closedControl.ConnectionStateChanged(p->_connectionStateChangedToken);
closedControl.WarningBell(p->_warningBellToken);
}
}
return false;
});
@ -2344,18 +2418,18 @@ std::optional<bool> Pane::PreCalculateCanSplit(const std::shared_ptr<Pane> targe
// Method Description:
// - Split the focused pane in our tree of panes, and place the given
// TermControl into the newly created pane. If we're the focused pane, then
// UserControl into the newly created pane. If we're the focused pane, then
// we'll create two new children, and place them side-by-side in our Grid.
// Arguments:
// - splitType: what type of split we want to create.
// - profile: The profile to associate with the newly created pane.
// - control: A TermControl to use in the new pane.
// - control: A UserControl to use in the new pane.
// Return Value:
// - The two newly created Panes, with the original pane first
std::pair<std::shared_ptr<Pane>, std::shared_ptr<Pane>> Pane::Split(SplitDirection splitType,
const float splitSize,
const Profile& profile,
const TermControl& control)
const Controls::UserControl& control)
{
if (!_lastActive)
{
@ -2463,13 +2537,16 @@ std::pair<std::shared_ptr<Pane>, std::shared_ptr<Pane>> Pane::_Split(SplitDirect
if (_IsLeaf())
{
// revoke our handler - the child will take care of the control now.
_control.ConnectionStateChanged(_connectionStateChangedToken);
_connectionStateChangedToken.value = 0;
_control.WarningBell(_warningBellToken);
_warningBellToken.value = 0;
if (const auto& termControl{ _control.try_as<TermControl>() })
{
// revoke our handler - the child will take care of the control now.
termControl.ConnectionStateChanged(_connectionStateChangedToken);
termControl.WarningBell(_warningBellToken);
_connectionStateChangedToken.value = 0;
_warningBellToken.value = 0;
}
// Remove our old GotFocus handler from the control. We don't want the
// Remove our old GotFocus handler from the control. We don't what the
// control telling us that it's now focused, we want it telling its new
// parent.
_gotFocusRevoker.revoke();
@ -2477,7 +2554,7 @@ std::pair<std::shared_ptr<Pane>, std::shared_ptr<Pane>> Pane::_Split(SplitDirect
}
// Remove any children we currently have. We can't add the existing
// TermControl to a new grid until we do this.
// UserControl to a new grid until we do this.
_root.Children().Clear();
_borderFirst.Child(nullptr);
_borderSecond.Child(nullptr);
@ -2855,8 +2932,13 @@ float Pane::CalcSnappedDimension(const bool widthOrHeight, const float dimension
// If requested size is already snapped, then both returned values equal this value.
Pane::SnapSizeResult Pane::_CalcSnappedDimension(const bool widthOrHeight, const float dimension) const
{
const auto& termControl{ _control.try_as<TermControl>() };
if (_IsLeaf())
{
if (!termControl)
{
return { dimension, dimension };
}
// If we're a leaf pane, align to the grid of controlling terminal
const auto minSize = _GetMinSize();
@ -2867,7 +2949,7 @@ Pane::SnapSizeResult Pane::_CalcSnappedDimension(const bool widthOrHeight, const
return { minDimension, minDimension };
}
float lower = _control.SnapDimensionToGrid(widthOrHeight, dimension);
float lower = termControl.SnapDimensionToGrid(widthOrHeight, dimension);
if (widthOrHeight)
{
lower += WI_IsFlagSet(_borders, Borders::Left) ? PaneBorderSize : 0;
@ -2887,7 +2969,7 @@ Pane::SnapSizeResult Pane::_CalcSnappedDimension(const bool widthOrHeight, const
}
else
{
const auto cellSize = _control.CharacterDimensions();
const auto cellSize = termControl.CharacterDimensions();
const auto higher = lower + (widthOrHeight ? cellSize.Width : cellSize.Height);
return { lower, higher };
}
@ -2932,26 +3014,39 @@ Pane::SnapSizeResult Pane::_CalcSnappedDimension(const bool widthOrHeight, const
// - <none>
void Pane::_AdvanceSnappedDimension(const bool widthOrHeight, LayoutSizeNode& sizeNode) const
{
const auto& termControl{ _control.try_as<TermControl>() };
if (_IsLeaf())
{
// We're a leaf pane, so just add one more row or column (unless isMinimumSize
// is true, see below).
if (sizeNode.isMinimumSize)
if (termControl)
{
// If the node is of its minimum size, this size might not be snapped (it might
// be, say, half a character, or fixed 10 pixels), so snap it upward. It might
// however be already snapped, so add 1 to make sure it really increases
// (not strictly necessary but to avoid surprises).
sizeNode.size = _CalcSnappedDimension(widthOrHeight, sizeNode.size + 1).higher;
// We're a leaf pane, so just add one more row or column (unless isMinimumSize
// is true, see below).
if (sizeNode.isMinimumSize)
{
// If the node is of its minimum size, this size might not be snapped (it might
// be, say, half a character, or fixed 10 pixels), so snap it upward. It might
// however be already snapped, so add 1 to make sure it really increases
// (not strictly necessary but to avoid surprises).
sizeNode.size = _CalcSnappedDimension(widthOrHeight, sizeNode.size + 1).higher;
}
else
{
const auto cellSize = termControl.CharacterDimensions();
sizeNode.size += widthOrHeight ? cellSize.Width : cellSize.Height;
}
}
else
{
const auto cellSize = _control.CharacterDimensions();
sizeNode.size += widthOrHeight ? cellSize.Width : cellSize.Height;
// If we're a leaf that didn't have a TermControl, then just increment
// by one. We have to increment by _some_ value, because this is used in
// a while() loop to find the next bigger size we can snap to. But since
// a non-terminal control doesn't really care what size it's snapped to,
// we can just say "one pixel larger is the next snap point"
sizeNode.size += 1;
}
}
else
else // !_IsLeaf()
{
// We're a parent pane, so we have to advance dimension of our children panes. In
// fact, we advance only one child (chosen later) to keep the growth fine-grained.
@ -3053,7 +3148,8 @@ Size Pane::_GetMinSize() const
{
if (_IsLeaf())
{
auto controlSize = _control.MinimumSize();
const auto& termControl{ _control.try_as<TermControl>() };
auto controlSize = termControl ? termControl.MinimumSize() : Size{ 1, 1 };
auto newWidth = controlSize.Width;
auto newHeight = controlSize.Height;
@ -3239,7 +3335,10 @@ std::optional<SplitDirection> Pane::PreCalculateAutoSplit(const std::shared_ptr<
// - Returns true if the pane or one of its descendants is read-only
bool Pane::ContainsReadOnly() const
{
return _IsLeaf() ? _control.ReadOnly() : (_firstChild->ContainsReadOnly() || _secondChild->ContainsReadOnly());
const auto& termControl{ GetTerminalControl() };
return termControl ?
termControl.ReadOnly() :
(_IsLeaf() ? false : (_firstChild->ContainsReadOnly() || _secondChild->ContainsReadOnly()));
}
// Method Description:
@ -3252,13 +3351,14 @@ bool Pane::ContainsReadOnly() const
// - <none>
void Pane::CollectTaskbarStates(std::vector<winrt::TerminalApp::TaskbarState>& states)
{
if (_IsLeaf())
const auto& termControl{ GetTerminalControl() };
if (termControl)
{
auto tbState{ winrt::make<winrt::TerminalApp::implementation::TaskbarState>(_control.TaskbarState(),
_control.TaskbarProgress()) };
auto tbState{ winrt::make<winrt::TerminalApp::implementation::TaskbarState>(termControl.TaskbarState(),
termControl.TaskbarProgress()) };
states.push_back(tbState);
}
else
else if (!_IsLeaf())
{
_firstChild->CollectTaskbarStates(states);
_secondChild->CollectTaskbarStates(states);

View file

@ -56,7 +56,7 @@ class Pane : public std::enable_shared_from_this<Pane>
{
public:
Pane(const winrt::Microsoft::Terminal::Settings::Model::Profile& profile,
const winrt::Microsoft::Terminal::Control::TermControl& control,
const winrt::Windows::UI::Xaml::Controls::UserControl& control,
const bool lastFocused = false);
Pane(std::shared_ptr<Pane> first,
@ -66,8 +66,9 @@ public:
const bool lastFocused = false);
std::shared_ptr<Pane> GetActivePane();
winrt::Windows::UI::Xaml::Controls::UserControl GetUserControl() const;
winrt::Microsoft::Terminal::Control::TermControl GetLastFocusedTerminalControl();
winrt::Microsoft::Terminal::Control::TermControl GetTerminalControl();
winrt::Microsoft::Terminal::Control::TermControl GetTerminalControl() const;
winrt::Microsoft::Terminal::Settings::Model::Profile GetFocusedProfile();
// Method Description:
@ -111,7 +112,7 @@ public:
std::pair<std::shared_ptr<Pane>, std::shared_ptr<Pane>> Split(winrt::Microsoft::Terminal::Settings::Model::SplitDirection splitType,
const float splitSize,
const winrt::Microsoft::Terminal::Settings::Model::Profile& profile,
const winrt::Microsoft::Terminal::Control::TermControl& control);
const winrt::Windows::UI::Xaml::Controls::UserControl& control);
bool ToggleSplitOrientation();
float CalcSnappedDimension(const bool widthOrHeight, const float dimension) const;
std::optional<winrt::Microsoft::Terminal::Settings::Model::SplitDirection> PreCalculateAutoSplit(const std::shared_ptr<Pane> target,
@ -127,6 +128,8 @@ public:
winrt::Microsoft::Terminal::Settings::Model::SplitDirection splitType);
std::shared_ptr<Pane> DetachPane(std::shared_ptr<Pane> pane);
winrt::Windows::UI::Xaml::Controls::UserControl ReplaceControl(const winrt::Windows::UI::Xaml::Controls::UserControl& control);
int GetLeafPaneCount() const noexcept;
void Maximize(std::shared_ptr<Pane> zoomedPane);
@ -201,7 +204,8 @@ private:
winrt::Windows::UI::Xaml::Controls::Grid _root{};
winrt::Windows::UI::Xaml::Controls::Border _borderFirst{};
winrt::Windows::UI::Xaml::Controls::Border _borderSecond{};
winrt::Microsoft::Terminal::Control::TermControl _control{ nullptr };
winrt::Windows::UI::Xaml::Controls::UserControl _control{ nullptr };
winrt::Microsoft::Terminal::TerminalConnection::ConnectionState _connectionState{ winrt::Microsoft::Terminal::TerminalConnection::ConnectionState::NotConnected };
static winrt::Windows::UI::Xaml::Media::SolidColorBrush s_focusedBorderBrush;
static winrt::Windows::UI::Xaml::Media::SolidColorBrush s_unfocusedBorderBrush;

View file

@ -504,6 +504,21 @@
<data name="MultiLinePasteDialog.Title" xml:space="preserve">
<value>Warning</value>
</data>
<data name="ApproveCommandlineWarning_CancelButton.Content" xml:space="preserve">
<value>Cancel</value>
</data>
<data name="ApproveCommandlineWarningPrefixTextBlock.Text" xml:space="preserve">
<value>You are about to execute the following commandline:</value>
</data>
<data name="ApproveCommandlineWarningSuffixTextBlock.Text" xml:space="preserve">
<value>Do you wish to continue?</value>
</data>
<data name="ApproveCommandlineWarning_PrimaryButton.Content" xml:space="preserve">
<value>Allow Commandline</value>
</data>
<data name="ApproveCommandlineWarningTitle.Text" xml:space="preserve">
<value>Warning</value>
</data>
<data name="CommandPalette_SearchBox.PlaceholderText" xml:space="preserve">
<value>Type a command name...</value>
</data>
@ -549,6 +564,9 @@
<value>Enter a wt commandline to run</value>
<comment>{Locked="wt"} </comment>
</data>
<data name="AdminWarningPlaceholderControlName" xml:space="preserve">
<value>Admin Warning Dialog</value>
</data>
<data name="CrimsonColorButton.[using:Windows.UI.Xaml.Controls]ToolTipService.ToolTip" xml:space="preserve">
<value>Crimson</value>
</data>

View file

@ -16,6 +16,7 @@
#include <LibraryResources.h>
#include "TabRowControl.h"
#include "AdminWarningPlaceholder.h"
#include "ColorHelper.h"
#include "DebugTapConnection.h"
#include "SettingsTab.h"
@ -248,7 +249,7 @@ namespace winrt::TerminalApp::implementation
// - profile: profile settings for this connection
// - settings: the TerminalSettings object to use to create the TerminalControl with.
// - existingConnection: optionally receives a connection from the outside world instead of attempting to create one
void TerminalPage::_CreateNewTabWithProfileAndSettings(const Profile& profile, const TerminalSettingsCreateResult& settings, TerminalConnection::ITerminalConnection existingConnection)
void TerminalPage::_CreateNewTabWithProfileAndSettings(Microsoft::Terminal::Settings::Model::Profile profile, Microsoft::Terminal::Settings::Model::TerminalSettingsCreateResult settings, TerminalConnection::ITerminalConnection existingConnection)
{
// Initialize the new tab
// Create a connection based on the values in our settings object if we weren't given one.
@ -279,8 +280,19 @@ namespace winrt::TerminalApp::implementation
// Give term control a child of the settings so that any overrides go in the child
// This way, when we do a settings reload we just update the parent and the overrides remain
auto term = _InitControl(settings, connection);
WUX::Controls::UserControl controlToAdd{ term };
auto newTabImpl = winrt::make_self<TerminalTab>(profile, term);
const auto& cmdline{ settings.DefaultSettings().Commandline() };
const bool doAdminWarning = _shouldPromptForCommandline(cmdline);
if (doAdminWarning)
{
auto warningControl{ winrt::make_self<implementation::AdminWarningPlaceholder>(term, cmdline) };
warningControl->PrimaryButtonClicked({ get_weak(), &TerminalPage::_adminWarningPrimaryClicked });
warningControl->CancelButtonClicked({ get_weak(), &TerminalPage::_adminWarningCancelClicked });
controlToAdd = *warningControl;
}
auto newTabImpl = winrt::make_self<TerminalTab>(profile, controlToAdd);
_RegisterTerminalEvents(term);
_InitializeTab(newTabImpl);

View file

@ -63,6 +63,9 @@
<Page Include="CommandPalette.xaml">
<SubType>Designer</SubType>
</Page>
<Page Include="AdminWarningPlaceholder.xaml">
<SubType>Designer</SubType>
</Page>
</ItemGroup>
<!-- ========================= Headers ======================== -->
<ItemGroup>
@ -141,6 +144,9 @@
<ClInclude Include="AppLogic.h">
<DependentUpon>AppLogic.idl</DependentUpon>
</ClInclude>
<ClInclude Include="AdminWarningPlaceholder.h">
<DependentUpon>AdminWarningPlaceholder.xaml</DependentUpon>
</ClInclude>
<ClInclude Include="Toast.h" />
</ItemGroup>
<!-- ========================= Cpp Files ======================== -->
@ -234,6 +240,9 @@
<ClCompile Include="AppLogic.cpp">
<DependentUpon>AppLogic.idl</DependentUpon>
</ClCompile>
<ClCompile Include="AdminWarningPlaceholder.cpp">
<DependentUpon>AdminWarningPlaceholder.xaml</DependentUpon>
</ClCompile>
<ClCompile Include="$(GeneratedFilesDir)module.g.cpp" />
<ClCompile Include="Toast.cpp" />
</ItemGroup>
@ -295,6 +304,10 @@
<DependentUpon>CommandPalette.xaml</DependentUpon>
<SubType>Code</SubType>
</Midl>
<Midl Include="AdminWarningPlaceholder.idl">
<DependentUpon>AdminWarningPlaceholder.xaml</DependentUpon>
<SubType>Code</SubType>
</Midl>
<Midl Include="FilteredCommand.idl" />
<Midl Include="EmptyStringVisibilityConverter.idl" />
</ItemGroup>

View file

@ -17,6 +17,7 @@
#include "DebugTapConnection.h"
#include "SettingsTab.h"
#include "RenameWindowRequestedArgs.g.cpp"
#include "AdminWarningPlaceholder.h"
#include "../inc/WindowingBehavior.h"
#include <til/latch.h>
@ -298,10 +299,7 @@ namespace winrt::TerminalApp::implementation
// - true if the ApplicationState should be used.
bool TerminalPage::ShouldUsePersistedLayout(CascadiaSettings& settings) const
{
// GH#5000 Until there is a separate state file for elevated sessions we should just not
// save at all while in an elevated window.
return Feature_PersistedWindowLayout::IsEnabled() &&
!IsElevated() &&
settings.GlobalSettings().FirstWindowPreference() == FirstWindowPreference::PersistedWindowLayout;
}
@ -655,6 +653,15 @@ namespace winrt::TerminalApp::implementation
co_return ContentDialogResult::None;
}
winrt::Windows::Foundation::IAsyncOperation<ContentDialogResult> TerminalPage::_ShowCommandlineApproveWarning()
{
if (auto presenter{ _dialogPresenter.get() })
{
co_return co_await presenter.ShowDialog(FindName(L"ApproveCommandlineWarning").try_as<WUX::Controls::ContentDialog>());
}
co_return ContentDialogResult::None;
}
// Method Description:
// - Builds the flyout (dropdown) attached to the new tab button, and
// attaches it to the button. Populates the flyout with one entry per
@ -1481,6 +1488,252 @@ namespace winrt::TerminalApp::implementation
return true;
}
// Function Description:
// - Returns true if this commandline is precisely an executable in
// system32. We can use this to bypass the elevated state check, because
// we're confident that executables in that path won't have been hijacked.
// - TECHNICALLY a user can take ownership of a file in system32 and
// replace it as the system administrator. You could say it's OK though
// because you'd already have to have had admin rights to mess that
// folder up or something.
// - Will attempt to resolve environment strings.
// - Will also manually allow commandlines as generated for the default WSL
// distros.
// Arguments:
// - commandLine: the command to check.
// Return Value (example):
// - C:\windows\system32\cmd.exe -> returns true
// - cmd.exe -> returns false
// - C:\windows\system32\cmd.exe /k echo sneaky sneak -> returns false
// - %SystemRoot%\System32\cmd.exe -> returns true
// - %SystemRoot%\System32\wsl.exe -d <distro name> -> returns true
static bool _isInSystem32(std::wstring_view commandLine)
{
// use C++11 magic statics to make sure we only do this once.
static std::wstring systemDirectory = []() -> std::wstring {
// *** THIS IS A SINGLETON ***
static std::wstring sys32{};
if (FAILED(wil::GetSystemDirectoryW(sys32)))
{
// we couldn't look up where system32 is?? Then it's definitely not
// in System32
return {};
}
return sys32;
}();
if (systemDirectory.empty())
{
return false;
}
const std::filesystem::path fullCommandlinePath{
wil::ExpandEnvironmentStringsW<std::wstring>(commandLine.data())
};
if (fullCommandlinePath.wstring().size() > systemDirectory.size())
{
// Get the first part of the executable path
const auto start = fullCommandlinePath.wstring().substr(0, systemDirectory.size());
// Doing this as an ASCII only check might be wrong, but I'm
// guessing if system32 isn't at X:\windows\system32... this isn't
// the only thing that is going to be sad in Windows.
const auto pathEquals = til::equals_insensitive_ascii(start, systemDirectory);
if (pathEquals && std::filesystem::exists(fullCommandlinePath))
{
return true;
}
}
// Also, if the path is literally
// %SystemRoot%\System32\wsl.exe -d <distro name>
// then allow it.
// Largely stolen from _tryMangleStartingDirectoryForWSL in ConptyConnection.
// Find the first space, quote or the end of the string -- we'll look
// for wsl before that.
const auto terminator{ commandLine.find_first_of(LR"(" )", 1) }; // look past the first character in case it starts with "
const auto start{ til::at(commandLine, 0) == L'"' ? 1 : 0 };
const std::filesystem::path executablePath{ commandLine.substr(start, terminator - start) };
const auto executableFilename{ executablePath.filename().wstring() };
if (executableFilename == L"wsl" || executableFilename == L"wsl.exe")
{
// We've got a WSL -- let's just make sure it's the right one.
if (executablePath.has_parent_path())
{
if (executablePath.parent_path().wstring() != systemDirectory)
{
return false; // it wasn't in system32!
}
}
else
{
// Unqualified WSL, this is dangerous, so return false.
return false;
}
// Get everything after the wsl.exe
const auto arguments{ terminator == std::wstring_view::npos ?
std::wstring_view{} :
commandLine.substr(terminator + 1) };
const auto dashD{ arguments.find(L"-d ") };
// If we found a "-d " IMMEDIATELY AFTER wsl.exe. If it wasn't
// immediately after, it could have been `wsl.exe --doSomethingEvil`
if (dashD == 0)
{
// Using the string following "-d "...
const auto afterDashD{ arguments.substr(dashD + 3) };
// Find the next space
const auto afterFirstWord = afterDashD.substr(dashD + 3).find(L" ");
// if that space _wasn't_ at the end of the commandline, then
// there were some other args. That means it was `wsl -d distro
// anything`, and we should ask the user.
//
// So if it was at the end of the commandline, then there were
// no other args besides the distro name.
if (afterFirstWord == std::wstring::npos)
{
return true;
}
}
}
return false;
}
// Method Description:
// - For a given commandline, determines if we should prompt the user for
// approval. We only do this check when elevated. This will check the
// AllowedCommandlines in `elevated-state.json`, to see if the commandline
// already exists in that list.
// Arguments:
// - cmdline: The commandline to check
// Return Value:
// - true if we should prompt the user for approval.
bool TerminalPage::_shouldPromptForCommandline(const winrt::hstring& cmdline) const
{
// NOTE: For debugging purposes, changing this to `true || IsElevated()`
// is a handy way of forcing the elevation logic, even when unelevated.
if (true || IsElevated())
{
// If the cmdline is EXACTLY an executable in
// `C:\WINDOWS\System32`, then ignore this check.
if (_isInSystem32(cmdline))
{
return false;
}
if (const auto& allowedCommandlines{ ApplicationState::SharedInstance().AllowedCommandlines() })
{
for (const auto& approved : allowedCommandlines)
{
if (approved == cmdline)
{
return false;
}
}
}
return true;
}
return false;
}
void TerminalPage::_adminWarningPrimaryClicked(const TerminalApp::AdminWarningPlaceholder& sender,
const winrt::Windows::UI::Xaml::RoutedEventArgs& /*args*/)
{
auto warningControl{ winrt::get_self<AdminWarningPlaceholder>(sender) };
const auto& cmdline{ warningControl->Commandline() };
// Look through the tabs and panes to look for us. Whichever pane had us
// as content - replace their content with the TermControl we were
// holding on to.
for (const auto& tab : _tabs)
{
if (const auto& tabImpl{ _GetTerminalTabImpl(tab) })
{
tabImpl->GetRootPane()->WalkTree([warningControl, cmdline, tabImpl](std::shared_ptr<Pane> pane) -> bool {
if (pane->GetUserControl() == *warningControl)
{
// Hooray, we found us!
pane->ReplaceControl(warningControl->Control());
// Update the title, because replacing the control like
// this is a little weird, and doesn't actually trigger
// a TitleChanged by itself.
tabImpl->UpdateTitle();
// Don't return true here. We want to make sure to check
// all the panes for the same commandline we just
// approved.
}
else if (const auto& otherWarning{ winrt::get_self<AdminWarningPlaceholder>(pane->GetUserControl().try_as<TerminalApp::AdminWarningPlaceholder>()) })
{
// This pane wasn't us, but it did have a warning in it.
// Was it a warning for the same commandline that we
// just approved?
if (otherWarning->Commandline() == cmdline)
{
// Go ahead and allow them as well.
pane->ReplaceControl(otherWarning->Control());
tabImpl->UpdateTitle();
}
}
// return false so we make sure to iterate on every leaf.
return false;
});
}
}
// Update the list of approved commandlines.
auto allowedCommandlines{ ApplicationState::SharedInstance().AllowedCommandlines() };
if (!allowedCommandlines)
{
allowedCommandlines = winrt::single_threaded_vector<winrt::hstring>();
}
// But of course, we don't need to add this commandline if it's already
// in the list of approved commandlines.
bool foundCopy = false;
for (const auto& approved : allowedCommandlines)
{
if (approved == cmdline)
{
foundCopy = true;
break;
}
}
if (!foundCopy)
{
allowedCommandlines.Append(cmdline);
}
ApplicationState::SharedInstance().AllowedCommandlines(allowedCommandlines);
}
void TerminalPage::_adminWarningCancelClicked(const TerminalApp::AdminWarningPlaceholder& sender,
const winrt::Windows::UI::Xaml::RoutedEventArgs& /*args*/)
{
auto warningControl{ winrt::get_self<AdminWarningPlaceholder>(sender) };
for (const auto& tab : _tabs)
{
if (const auto& tabImpl{ _GetTerminalTabImpl(tab) })
{
tabImpl->GetRootPane()->WalkTree([warningControl](std::shared_ptr<Pane> pane) -> bool {
if (pane->GetUserControl() == *warningControl)
{
pane->Close();
return true;
}
// We're not going to auto-close all the other panes with
// the same commandline warning, akin to what we're doing in
// the approve handler. If they want to reject one pane, but
// accept the next one, that's okay.
return false;
});
}
}
}
// Method Description:
// - Split the focused pane either horizontally or vertically, and place the
// given TermControl into the newly created pane.
@ -1582,20 +1835,33 @@ namespace winrt::TerminalApp::implementation
}
auto newControl = _InitControl(controlSettings, controlConnection);
WUX::Controls::UserControl controlToAdd{ newControl };
const auto& cmdline{ controlSettings.DefaultSettings().Commandline() };
if (_shouldPromptForCommandline(cmdline))
{
auto warningControl{ winrt::make_self<implementation::AdminWarningPlaceholder>(newControl, cmdline) };
warningControl->PrimaryButtonClicked({ get_weak(), &TerminalPage::_adminWarningPrimaryClicked });
warningControl->CancelButtonClicked({ get_weak(), &TerminalPage::_adminWarningCancelClicked });
controlToAdd = *warningControl;
}
// Hookup our event handlers to the new terminal
_RegisterTerminalEvents(newControl);
_UnZoomIfNeeded();
tab.SplitPane(realSplitType, splitSize, profile, newControl);
tab.SplitPane(realSplitType, splitSize, profile, controlToAdd);
// After GH#6586, the control will no longer focus itself
// automatically when it's finished being laid out. Manually focus
// the control here instead.
if (_startupState == StartupState::Initialized)
{
_GetActiveControl().Focus(FocusState::Programmatic);
if (const auto& activeControl{ _GetActiveControl() })
{
activeControl.Focus(FocusState::Programmatic);
}
}
}
CATCH_LOG();

View file

@ -203,12 +203,13 @@ namespace winrt::TerminalApp::implementation
winrt::Windows::Foundation::IAsyncOperation<winrt::Windows::UI::Xaml::Controls::ContentDialogResult> _ShowCloseReadOnlyDialog();
winrt::Windows::Foundation::IAsyncOperation<winrt::Windows::UI::Xaml::Controls::ContentDialogResult> _ShowMultiLinePasteWarningDialog();
winrt::Windows::Foundation::IAsyncOperation<winrt::Windows::UI::Xaml::Controls::ContentDialogResult> _ShowLargePasteWarningDialog();
winrt::Windows::Foundation::IAsyncOperation<winrt::Windows::UI::Xaml::Controls::ContentDialogResult> _ShowCommandlineApproveWarning();
void _CreateNewTabFlyout();
void _OpenNewTabDropdown();
HRESULT _OpenNewTab(const Microsoft::Terminal::Settings::Model::NewTerminalArgs& newTerminalArgs, winrt::Microsoft::Terminal::TerminalConnection::ITerminalConnection existingConnection = nullptr);
void _CreateNewTabFromPane(std::shared_ptr<Pane> pane);
void _CreateNewTabWithProfileAndSettings(const Microsoft::Terminal::Settings::Model::Profile& profile, const Microsoft::Terminal::Settings::Model::TerminalSettingsCreateResult& settings, winrt::Microsoft::Terminal::TerminalConnection::ITerminalConnection existingConnection = nullptr);
void _CreateNewTabWithProfileAndSettings(Microsoft::Terminal::Settings::Model::Profile profile, Microsoft::Terminal::Settings::Model::TerminalSettingsCreateResult settings, winrt::Microsoft::Terminal::TerminalConnection::ITerminalConnection existingConnection = nullptr);
winrt::Microsoft::Terminal::TerminalConnection::ITerminalConnection _CreateConnectionFromSettings(Microsoft::Terminal::Settings::Model::Profile profile, Microsoft::Terminal::Settings::Model::TerminalSettings settings);
winrt::fire_and_forget _OpenNewWindow(const bool elevate, const Microsoft::Terminal::Settings::Model::NewTerminalArgs newTerminalArgs);
@ -400,6 +401,12 @@ namespace winrt::TerminalApp::implementation
winrt::Microsoft::Terminal::Settings::Model::Profile GetClosestProfileForDuplicationOfProfile(const winrt::Microsoft::Terminal::Settings::Model::Profile& profile) const noexcept;
bool _shouldPromptForCommandline(const winrt::hstring& cmdline) const;
void _adminWarningPrimaryClicked(const winrt::TerminalApp::AdminWarningPlaceholder& sender,
const winrt::Windows::UI::Xaml::RoutedEventArgs& args);
void _adminWarningCancelClicked(const winrt::TerminalApp::AdminWarningPlaceholder& sender,
const winrt::Windows::UI::Xaml::RoutedEventArgs& args);
winrt::fire_and_forget _ConnectionStateChangedHandler(const winrt::Windows::Foundation::IInspectable& sender, const winrt::Windows::Foundation::IInspectable& args) const;
void _CloseOnExitInfoDismissHandler(const winrt::Windows::Foundation::IInspectable& sender, const winrt::Windows::Foundation::IInspectable& args) const;
void _KeyboardServiceWarningInfoDismissHandler(const winrt::Windows::Foundation::IInspectable& sender, const winrt::Windows::Foundation::IInspectable& args) const;

View file

@ -25,7 +25,7 @@ namespace winrt
namespace winrt::TerminalApp::implementation
{
TerminalTab::TerminalTab(const Profile& profile, const TermControl& control)
TerminalTab::TerminalTab(const Profile& profile, const WUX::Controls::UserControl& control)
{
_rootPane = std::make_shared<Pane>(profile, control, true);
@ -437,7 +437,10 @@ namespace winrt::TerminalApp::implementation
winrt::fire_and_forget TerminalTab::Scroll(const int delta)
{
auto control = GetActiveTerminalControl();
if (!control)
{
co_return;
}
co_await winrt::resume_foreground(control.Dispatcher());
const auto currentOffset = control.ScrollOffset();
@ -513,7 +516,7 @@ namespace winrt::TerminalApp::implementation
void TerminalTab::SplitPane(SplitDirection splitType,
const float splitSize,
const Profile& profile,
TermControl& control)
const WUX::Controls::UserControl& control)
{
// Make sure to take the ID before calling Split() - Split() will clear out the active pane's ID
const auto activePaneId = _activePane->Id();
@ -533,7 +536,10 @@ namespace winrt::TerminalApp::implementation
// Add a event handlers to the new panes' GotFocus event. When the pane
// gains focus, we'll mark it as the new active pane.
_AttachEventHandlersToControl(newPane->Id().value(), control);
const auto& termControl{ control.try_as<TermControl>() };
// _AttachEventHandlersToControl will immediately bail if the provided
// control is null (READ: if the pane didn't have a TermControl)
_AttachEventHandlersToControl(newPane->Id().value(), termControl);
_AttachEventHandlersToPane(original);
_AttachEventHandlersToPane(newPane);
@ -858,6 +864,10 @@ namespace winrt::TerminalApp::implementation
// - <none>
void TerminalTab::_AttachEventHandlersToControl(const uint32_t paneId, const TermControl& control)
{
if (!control)
{
return;
}
auto weakThis{ get_weak() };
auto dispatcher = TabViewItem().Dispatcher();
ControlEventTokens events{};

View file

@ -21,7 +21,8 @@ namespace winrt::TerminalApp::implementation
struct TerminalTab : TerminalTabT<TerminalTab, TabBase>
{
public:
TerminalTab(const winrt::Microsoft::Terminal::Settings::Model::Profile& profile, const winrt::Microsoft::Terminal::Control::TermControl& control);
TerminalTab(const winrt::Microsoft::Terminal::Settings::Model::Profile& profile,
const winrt::Windows::UI::Xaml::Controls::UserControl& control);
TerminalTab(std::shared_ptr<Pane> rootPane);
// Called after construction to perform the necessary setup, which relies on weak_ptr
@ -41,7 +42,7 @@ namespace winrt::TerminalApp::implementation
void SplitPane(winrt::Microsoft::Terminal::Settings::Model::SplitDirection splitType,
const float splitSize,
const winrt::Microsoft::Terminal::Settings::Model::Profile& profile,
winrt::Microsoft::Terminal::Control::TermControl& control);
const winrt::Windows::UI::Xaml::Controls::UserControl& control);
void ToggleSplitOrientation();
winrt::fire_and_forget UpdateIcon(const winrt::hstring iconPath);

View file

@ -2590,4 +2590,9 @@ namespace winrt::Microsoft::Terminal::Control::implementation
{
return _core.ReadEntireBuffer();
}
Media::Brush TermControl::BackgroundBrush()
{
return RootGrid().Background();
}
}

View file

@ -111,6 +111,8 @@ namespace winrt::Microsoft::Terminal::Control::implementation
hstring ReadEntireBuffer() const;
Windows::UI::Xaml::Media::Brush BackgroundBrush();
// -------------------------------- WinRT Events ---------------------------------
// clang-format off
WINRT_CALLBACK(FontSizeChanged, Control::FontSizeChangedEventArgs);

View file

@ -71,5 +71,7 @@ namespace Microsoft.Terminal.Control
void ToggleReadOnly();
String ReadEntireBuffer();
Windows.UI.Xaml.Media.Brush BackgroundBrush { get; };
}
}

View file

@ -9,8 +9,11 @@
#include "ActionAndArgs.h"
#include "JsonUtils.h"
#include "FileUtils.h"
#include "../../types/inc/utils.hpp"
static constexpr std::wstring_view stateFileName{ L"state.json" };
static constexpr std::wstring_view elevatedStateFileName{ L"elevated-state.json" };
static constexpr std::string_view TabLayoutKey{ "tabLayout" };
static constexpr std::string_view InitialPositionKey{ "initialPosition" };
static constexpr std::string_view InitialSizeKey{ "initialSize" };
@ -85,15 +88,9 @@ namespace winrt::Microsoft::Terminal::Settings::Model::implementation
return trait.FromJson(root);
}
// Returns the application-global ApplicationState object.
Microsoft::Terminal::Settings::Model::ApplicationState ApplicationState::SharedInstance()
{
static auto state = winrt::make_self<ApplicationState>(GetBaseSettingsPath() / stateFileName);
return *state;
}
ApplicationState::ApplicationState(std::filesystem::path path) noexcept :
_path{ std::move(path) },
ApplicationState::ApplicationState(const std::filesystem::path& stateRoot) noexcept :
_sharedPath{ stateRoot / stateFileName },
_elevatedPath{ stateRoot / elevatedStateFileName },
_throttler{ std::chrono::seconds(1), [this]() { _write(); } }
{
_read();
@ -102,9 +99,21 @@ namespace winrt::Microsoft::Terminal::Settings::Model::implementation
// The destructor ensures that the last write is flushed to disk before returning.
ApplicationState::~ApplicationState()
{
TraceLoggingWrite(g_hSettingsModelProvider,
"ApplicationState_Dtor_Start",
TraceLoggingDescription("Event at the start of the ApplicationState destructor"),
TraceLoggingLevel(WINEVENT_LEVEL_VERBOSE),
TraceLoggingKeyword(TIL_KEYWORD_TRACE));
// This will ensure that we not just cancel the last outstanding timer,
// but instead force it to run as soon as possible and wait for it to complete.
_throttler.flush();
TraceLoggingWrite(g_hSettingsModelProvider,
"ApplicationState_Dtor_End",
TraceLoggingDescription("Event at the end of the ApplicationState destructor"),
TraceLoggingLevel(WINEVENT_LEVEL_VERBOSE),
TraceLoggingKeyword(TIL_KEYWORD_TRACE));
}
// Re-read the state.json from disk.
@ -113,80 +122,59 @@ namespace winrt::Microsoft::Terminal::Settings::Model::implementation
_read();
}
// Returns the state.json path on the disk.
winrt::hstring ApplicationState::FilePath() const noexcept
bool ApplicationState::IsStatePath(const winrt::hstring& filename)
{
return winrt::hstring{ _path.wstring() };
static const auto sharedPath{ _sharedPath.filename() };
static const auto elevatedPath{ _elevatedPath.filename() };
return filename == sharedPath || filename == elevatedPath;
}
// Generate all getter/setters
#define MTSM_APPLICATION_STATE_GEN(type, name, key, ...) \
type ApplicationState::name() const noexcept \
{ \
const auto state = _state.lock_shared(); \
const auto& value = state->name; \
return value ? *value : type{ __VA_ARGS__ }; \
} \
\
void ApplicationState::name(const type& value) noexcept \
{ \
{ \
auto state = _state.lock(); \
state->name.emplace(value); \
state->name##Changed = true; \
} \
\
_throttler(); \
}
MTSM_APPLICATION_STATE_FIELDS(MTSM_APPLICATION_STATE_GEN)
#undef MTSM_APPLICATION_STATE_GEN
Json::Value ApplicationState::_getRoot(const locked_hfile& file) const noexcept
{
Json::Value root;
try
{
const auto data = ReadUTF8FileLocked(file);
if (data.empty())
{
return root;
}
std::string errs;
std::unique_ptr<Json::CharReader> reader{ Json::CharReaderBuilder::CharReaderBuilder().newCharReader() };
if (!reader->parse(data.data(), data.data() + data.size(), &root, &errs))
{
throw winrt::hresult_error(WEB_E_INVALID_JSON_STRING, winrt::to_hstring(errs));
}
}
CATCH_LOG()
return root;
}
// Deserializes the state.json at _path into this ApplicationState.
// Deserializes the state.json and user-state (or elevated-state if
// elevated) into this ApplicationState.
// * ANY errors during app state will result in the creation of a new empty state.
// * ANY errors during runtime will result in changes being partially ignored.
void ApplicationState::_read() const noexcept
try
{
auto state = _state.lock();
const auto file = OpenFileReadSharedLocked(_path);
std::string errs;
std::unique_ptr<Json::CharReader> reader{ Json::CharReaderBuilder::CharReaderBuilder().newCharReader() };
auto root = _getRoot(file);
// GetValueForKey() comes in two variants:
// * take a std::optional<T> reference
// * return std::optional<T> by value
// At the time of writing the former version skips missing fields in the json,
// but we want to explicitly clear state fields that were removed from state.json.
#define MTSM_APPLICATION_STATE_GEN(type, name, key, ...) \
if (!state->name##Changed) \
{ \
state->name = JsonUtils::GetValueForKey<std::optional<type>>(root, key); \
}
MTSM_APPLICATION_STATE_FIELDS(MTSM_APPLICATION_STATE_GEN)
#undef MTSM_APPLICATION_STATE_GEN
// First get shared state out of `state.json`.
const auto sharedData = _readSharedContents().value_or(std::string{});
if (!sharedData.empty())
{
Json::Value root;
if (!reader->parse(sharedData.data(), sharedData.data() + sharedData.size(), &root, &errs))
{
throw winrt::hresult_error(WEB_E_INVALID_JSON_STRING, winrt::to_hstring(errs));
}
// - If we're elevated, we want to only load the Shared properties
// from state.json. We'll then load the Local props from
// `elevated-state.json`
// - If we're unelevated, then load _everything_ from state.json.
if (::Microsoft::Console::Utils::IsElevated())
{
// Only load shared properties if we're elevated
FromJson(root, FileSource::Shared);
// Then, try and get anything in elevated-state
if (const auto localData{ _readLocalContents().value_or(std::string{}) }; !localData.empty())
{
Json::Value root;
if (!reader->parse(localData.data(), localData.data() + localData.size(), &root, &errs))
{
throw winrt::hresult_error(WEB_E_INVALID_JSON_STRING, winrt::to_hstring(errs));
}
FromJson(root, FileSource::Local);
}
}
else
{
// If we're unelevated, then load everything.
FromJson(root, FileSource::Shared | FileSource::Local);
}
}
}
CATCH_LOG()
@ -194,29 +182,191 @@ namespace winrt::Microsoft::Terminal::Settings::Model::implementation
// * Errors are only logged.
// * _state->_writeScheduled is set to false, signaling our
// setters that _synchronize() needs to be called again.
void ApplicationState::_write() noexcept
void ApplicationState::_write() const noexcept
try
{
// re-read the state so that we can only update the properties that were changed.
Json::Value root{};
Json::StreamWriterBuilder wbuilder;
// When we're elevated, we've got to be tricky. We don't want to write
// our window state, allowed commandlines, and other Local properties
// into the shared `state.json`. But, if we only serialize the Shared
// properties to a json blob, then we'll omit windowState entirely,
// _removing_ the window state of the unelevated instance. Oh no!
//
// So, to be tricky, we'll first _load_ the shared state to a json blob.
// We'll then serialize our view of the shared properties on top of that
// blob. Then we'll write that blob back to the file. This will
// round-trip the Local properties for the unelevated instances
// untouched in state.json
//
// After that's done, we'll write our Local properties into
// elevated-state.json.
if (::Microsoft::Console::Utils::IsElevated())
{
auto state = _state.lock();
const auto file = OpenFileRWExclusiveLocked(_path);
root = _getRoot(file);
std::string errs;
std::unique_ptr<Json::CharReader> reader{ Json::CharReaderBuilder::CharReaderBuilder().newCharReader() };
Json::Value root;
#define MTSM_APPLICATION_STATE_GEN(type, name, key, ...) \
if (state->name##Changed) \
{ \
JsonUtils::SetValueForKey(root, key, state->name); \
state->name##Changed = false; \
}
MTSM_APPLICATION_STATE_FIELDS(MTSM_APPLICATION_STATE_GEN)
#undef MTSM_APPLICATION_STATE_GEN
// First load the contents of state.json into a json blob. This will
// contain the Shared properties and the unelevated instance's Local
// properties.
const auto sharedData = _readSharedContents().value_or(std::string{});
if (!sharedData.empty())
{
if (!reader->parse(sharedData.data(), sharedData.data() + sharedData.size(), &root, &errs))
{
throw winrt::hresult_error(WEB_E_INVALID_JSON_STRING, winrt::to_hstring(errs));
}
}
// Layer our shared properties on top of the blob from state.json,
// and write it back out.
_writeSharedContents(Json::writeString(wbuilder, _toJsonWithBlob(root, FileSource::Shared)));
Json::StreamWriterBuilder wbuilder;
const auto content = Json::writeString(wbuilder, root);
WriteUTF8FileLocked(file, content);
// Finally, write our Local properties back to elevated-state.json
_writeLocalContents(Json::writeString(wbuilder, ToJson(FileSource::Local)));
}
else
{
// We're unelevated, this is easy. Just write everything back out.
_writeLocalContents(Json::writeString(wbuilder, ToJson(FileSource::Local | FileSource::Shared)));
}
}
CATCH_LOG()
// Returns the application-global ApplicationState object.
Microsoft::Terminal::Settings::Model::ApplicationState ApplicationState::SharedInstance()
{
std::filesystem::path root{ GetBaseSettingsPath() };
static auto state = winrt::make_self<ApplicationState>(root);
return *state;
}
// Method Description:
// - Loads data from the given json blob. Will only read the data that's in
// the specified parseSource - so if we're reading the Local state file,
// we won't destroy previously parsed Shared data.
// - READ: there's no layering for app state.
void ApplicationState::FromJson(const Json::Value& root, FileSource parseSource) const noexcept
{
auto state = _state.lock();
// GetValueForKey() comes in two variants:
// * take a std::optional<T> reference
// * return std::optional<T> by value
// At the time of writing the former version skips missing fields in the json,
// but we want to explicitly clear state fields that were removed from state.json.
//
// GH#11222: We only load properties that are of the same type (Local or
// Shared) which we requested. If we didn't want to load this type of
// property, just skip it.
#define MTSM_APPLICATION_STATE_GEN(source, type, name, key, ...) \
if (WI_IsFlagSet(parseSource, source)) \
state->name = JsonUtils::GetValueForKey<std::optional<type>>(root, key);
MTSM_APPLICATION_STATE_FIELDS(MTSM_APPLICATION_STATE_GEN)
#undef MTSM_APPLICATION_STATE_GEN
}
Json::Value ApplicationState::ToJson(FileSource parseSource) const noexcept
{
Json::Value root{ Json::objectValue };
return _toJsonWithBlob(root, parseSource);
}
Json::Value ApplicationState::_toJsonWithBlob(Json::Value& root, FileSource parseSource) const noexcept
{
{
auto state = _state.lock_shared();
// GH#11222: We only write properties that are of the same type (Local
// or Shared) which we requested. If we didn't want to serialize this
// type of property, just skip it.
#define MTSM_APPLICATION_STATE_GEN(source, type, name, key, ...) \
if (WI_IsFlagSet(parseSource, source)) \
JsonUtils::SetValueForKey(root, key, state->name);
MTSM_APPLICATION_STATE_FIELDS(MTSM_APPLICATION_STATE_GEN)
#undef MTSM_APPLICATION_STATE_GEN
}
return root;
}
// Generate all getter/setters
#define MTSM_APPLICATION_STATE_GEN(source, type, name, key, ...) \
type ApplicationState::name() const noexcept \
{ \
const auto state = _state.lock_shared(); \
const auto& value = state->name; \
return value ? *value : type{ __VA_ARGS__ }; \
} \
\
void ApplicationState::name(const type& value) noexcept \
{ \
{ \
auto state = _state.lock(); \
state->name.emplace(value); \
} \
\
_throttler(); \
}
MTSM_APPLICATION_STATE_FIELDS(MTSM_APPLICATION_STATE_GEN)
#undef MTSM_APPLICATION_STATE_GEN
// Method Description:
// - Read the contents of our "shared" state - state that should be shared
// for elevated and unelevated instances. This is things like the list of
// generated profiles, the command palette commandlines.
std::optional<std::string> ApplicationState::_readSharedContents() const
{
return ReadUTF8FileIfExists(_sharedPath);
}
// Method Description:
// - Read the contents of our "local" state - state that should be kept in
// separate files for elevated and unelevated instances. This is things
// like the persisted window state, and the approved commandlines (though,
// those don't matter when unelevated).
// - When elevated, this will DELETE `elevated-state.json` if it has bad
// permissions, so we don't potentially read malicious data.
std::optional<std::string> ApplicationState::_readLocalContents() const
{
return ::Microsoft::Console::Utils::IsElevated() ?
ReadUTF8FileIfExists(_elevatedPath, true) :
ReadUTF8FileIfExists(_sharedPath, false);
}
// Method Description:
// - Write the contents of our "shared" state - state that should be shared
// for elevated and unelevated instances. This will atomically write to
// `state.json`
void ApplicationState::_writeSharedContents(const std::string_view content) const
{
WriteUTF8FileAtomic(_sharedPath, content);
}
// Method Description:
// - Write the contents of our "local" state - state that should be kept in
// separate files for elevated and unelevated instances. When elevated,
// this will write to `elevated-state.json`, and when unelevated, this
// will atomically write to `user-state.json`
void ApplicationState::_writeLocalContents(const std::string_view content) const
{
if (::Microsoft::Console::Utils::IsElevated())
{
// DON'T use WriteUTF8FileAtomic, which will write to a temporary file
// then rename that file to the final filename. That actually lets us
// overwrite the elevate file's contents even when unelevated, because
// we're effectively deleting the original file, then renaming a
// different file in it's place.
//
// We're not worried about someone else doing that though, if they do
// that with the wrong permissions, then we'll just ignore the file and
// start over.
WriteUTF8File(_elevatedPath, content, true);
}
else
{
WriteUTF8FileAtomic(_sharedPath, content);
}
}
}

View file

@ -16,21 +16,30 @@ Abstract:
#include "WindowLayout.g.h"
#include <inc/cppwinrt_utils.h>
#include <til/mutex.h>
#include <til/throttled_func.h>
#include "FileUtils.h"
#include <JsonUtils.h>
namespace winrt::Microsoft::Terminal::Settings::Model::implementation
{
// If a property is Shared, then it'll be stored in `state.json`, and used
// in both elevated and unelevated instances of the Terminal. If a property
// is marked Local, then it will have separate values for elevated and
// unelevated instances.
enum FileSource : int
{
Shared = 0x1,
Local = 0x2
};
DEFINE_ENUM_FLAG_OPERATORS(FileSource);
// This macro generates all getters and setters for ApplicationState.
// It provides X with the following arguments:
// (type, function name, JSON key, ...variadic construction arguments)
namespace winrt::Microsoft::Terminal::Settings::Model::implementation
{
#define MTSM_APPLICATION_STATE_FIELDS(X) \
X(std::unordered_set<winrt::guid>, GeneratedProfiles, "generatedProfiles") \
X(Windows::Foundation::Collections::IVector<Model::WindowLayout>, PersistedWindowLayouts, "persistedWindowLayouts") \
X(Windows::Foundation::Collections::IVector<hstring>, RecentCommands, "recentCommands") \
X(Windows::Foundation::Collections::IVector<winrt::Microsoft::Terminal::Settings::Model::InfoBarMessage>, DismissedMessages, "dismissedMessages")
// (source, type, function name, JSON key, ...variadic construction arguments)
#define MTSM_APPLICATION_STATE_FIELDS(X) \
X(FileSource::Shared, std::unordered_set<winrt::guid>, GeneratedProfiles, "generatedProfiles") \
X(FileSource::Local, Windows::Foundation::Collections::IVector<Model::WindowLayout>, PersistedWindowLayouts, "persistedWindowLayouts") \
X(FileSource::Shared, Windows::Foundation::Collections::IVector<hstring>, RecentCommands, "recentCommands") \
X(FileSource::Shared, Windows::Foundation::Collections::IVector<winrt::Microsoft::Terminal::Settings::Model::InfoBarMessage>, DismissedMessages, "dismissedMessages") \
X(FileSource::Local, Windows::Foundation::Collections::IVector<hstring>, AllowedCommandlines, "allowedCommandlines")
struct WindowLayout : WindowLayoutT<WindowLayout>
{
@ -44,22 +53,24 @@ namespace winrt::Microsoft::Terminal::Settings::Model::implementation
friend ::Microsoft::Terminal::Settings::Model::JsonUtils::ConversionTrait<Model::WindowLayout>;
};
struct ApplicationState : ApplicationStateT<ApplicationState>
struct ApplicationState : public ApplicationStateT<ApplicationState>
{
static Microsoft::Terminal::Settings::Model::ApplicationState SharedInstance();
ApplicationState(std::filesystem::path path) noexcept;
ApplicationState(const std::filesystem::path& stateRoot) noexcept;
~ApplicationState();
// Methods
void Reload() const noexcept;
void FromJson(const Json::Value& root, FileSource parseSource) const noexcept;
Json::Value ToJson(FileSource parseSource) const noexcept;
// General getters/setters
winrt::hstring FilePath() const noexcept;
bool IsStatePath(const winrt::hstring& filename);
// State getters/setters
#define MTSM_APPLICATION_STATE_GEN(type, name, key, ...) \
type name() const noexcept; \
#define MTSM_APPLICATION_STATE_GEN(source, type, name, key, ...) \
type name() const noexcept; \
void name(const type& value) noexcept;
MTSM_APPLICATION_STATE_FIELDS(MTSM_APPLICATION_STATE_GEN)
#undef MTSM_APPLICATION_STATE_GEN
@ -67,21 +78,25 @@ namespace winrt::Microsoft::Terminal::Settings::Model::implementation
private:
struct state_t
{
#define MTSM_APPLICATION_STATE_GEN(type, name, key, ...) \
std::optional<type> name{ __VA_ARGS__ }; \
bool name##Changed = false;
#define MTSM_APPLICATION_STATE_GEN(source, type, name, key, ...) std::optional<type> name{ __VA_ARGS__ };
MTSM_APPLICATION_STATE_FIELDS(MTSM_APPLICATION_STATE_GEN)
#undef MTSM_APPLICATION_STATE_GEN
};
til::shared_mutex<state_t> _state;
std::filesystem::path _sharedPath;
std::filesystem::path _userPath;
std::filesystem::path _elevatedPath;
til::throttled_func_trailing<> _throttler;
Json::Value _getRoot(const winrt::Microsoft::Terminal::Settings::Model::locked_hfile& file) const noexcept;
void _write() noexcept;
void _write() const noexcept;
void _read() const noexcept;
std::filesystem::path _path;
til::shared_mutex<state_t> _state;
til::throttled_func_trailing<> _throttler;
Json::Value _toJsonWithBlob(Json::Value& root, FileSource parseSource) const noexcept;
std::optional<std::string> _readSharedContents() const;
void _writeSharedContents(const std::string_view content) const;
std::optional<std::string> _readLocalContents() const;
void _writeLocalContents(const std::string_view content) const;
};
}

View file

@ -28,12 +28,15 @@ namespace Microsoft.Terminal.Settings.Model
void Reload();
String FilePath { get; };
Boolean IsStatePath(String filename);
Windows.Foundation.Collections.IVector<WindowLayout> PersistedWindowLayouts { get; set; };
Windows.Foundation.Collections.IVector<String> RecentCommands { get; set; };
Windows.Foundation.Collections.IVector<InfoBarMessage> DismissedMessages { get; set; };
Windows.Foundation.Collections.IVector<String> AllowedCommandlines { get; set; };
}
}

View file

@ -8,6 +8,8 @@
#include <shlobj.h>
#include <WtExeUtils.h>
#include <aclapi.h>
static constexpr std::string_view Utf8Bom{ u8"\uFEFF" };
static constexpr std::wstring_view UnpackagedSettingsFolderName{ L"Microsoft\\Windows Terminal\\" };
@ -39,87 +41,89 @@ namespace winrt::Microsoft::Terminal::Settings::Model
return baseSettingsPath;
}
locked_hfile OpenFileReadSharedLocked(const std::filesystem::path& path)
static bool _hasExpectedPermissions(const std::filesystem::path& path)
{
wil::unique_hfile file{ CreateFileW(path.c_str(), GENERIC_READ, FILE_SHARE_READ | FILE_SHARE_WRITE, nullptr, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, nullptr) };
THROW_LAST_ERROR_IF(!file);
// just lock the entire file
OVERLAPPED sOverlapped;
sOverlapped.Offset = 0;
sOverlapped.OffsetHigh = 0;
// Shared lock
THROW_LAST_ERROR_IF(!LockFileEx(file.get(),
0, // lock shared, wait to return until lock is obtained
0, // reserved, does nothing
INT_MAX, // lock INT_MAX bytes
0, // higher-order bytes, if our state file is greater than 2GB I guess this will be a problem
&sOverlapped));
return { std::move(file), sOverlapped };
}
// If we want to only open the file if it's elevated, check the
// permissions on this file. We want to make sure that:
// * Everyone has permission to read
// * admins can do anything
// * no one else can do anything.
PACL pAcl{ nullptr }; // This doesn't need to be cleanup up apparently
locked_hfile OpenFileRWExclusiveLocked(const std::filesystem::path& path)
{
wil::unique_hfile file{ CreateFileW(path.c_str(), GENERIC_READ | GENERIC_WRITE, FILE_SHARE_READ | FILE_SHARE_WRITE, nullptr, OPEN_ALWAYS, FILE_ATTRIBUTE_NORMAL, nullptr) };
THROW_LAST_ERROR_IF(!file);
// just lock the entire file
OVERLAPPED sOverlapped;
sOverlapped.Offset = 0;
sOverlapped.OffsetHigh = 0;
// Shared lock
THROW_LAST_ERROR_IF(!LockFileEx(file.get(),
LOCKFILE_EXCLUSIVE_LOCK, // lock exclusive, wait to return until lock is obtained
0, // reserved, does nothing
INT_MAX, // lock INT_MAX bytes
0, // higher-order bytes, if our state file is greater than 2GB I guess this will be a problem
&sOverlapped));
return { std::move(file), sOverlapped };
}
auto status = GetNamedSecurityInfo(path.c_str(),
SE_FILE_OBJECT,
DACL_SECURITY_INFORMATION,
nullptr,
nullptr,
&pAcl,
nullptr,
nullptr);
THROW_IF_WIN32_ERROR(status);
std::string ReadUTF8FileLocked(const locked_hfile& file)
{
const auto fileSize = GetFileSize(file.get(), nullptr);
THROW_LAST_ERROR_IF(fileSize == INVALID_FILE_SIZE);
PEXPLICIT_ACCESS pEA{ nullptr };
DWORD count = 0;
status = GetExplicitEntriesFromAcl(pAcl, &count, &pEA);
THROW_IF_WIN32_ERROR(status);
// By making our buffer just slightly larger we can detect if
// the file size changed and we've failed to read the full file.
std::string buffer(static_cast<size_t>(fileSize) + 1, '\0');
DWORD bytesRead = 0;
THROW_IF_WIN32_BOOL_FALSE(ReadFile(file.get(), buffer.data(), gsl::narrow<DWORD>(buffer.size()), &bytesRead, nullptr));
auto explicitAccessCleanup = wil::scope_exit([&]() { ::LocalFree(pEA); });
// As mentioned before our buffer was allocated oversized.
buffer.resize(bytesRead);
if (til::starts_with(buffer, Utf8Bom))
if (count != 2)
{
// Yeah this memmove()s the entire content.
// But I don't really want to deal with UTF8 BOMs any more than necessary,
// as basically not a single editor writes a BOM for UTF8.
buffer.erase(0, Utf8Bom.size());
return false;
}
return buffer;
// Now, get the Everyone and Admins SIDS so we can make sure they're
// the ones in this file.
wil::unique_sid everyoneSid;
wil::unique_sid adminGroupSid;
SID_IDENTIFIER_AUTHORITY SIDAuthNT = SECURITY_NT_AUTHORITY;
SID_IDENTIFIER_AUTHORITY SIDAuthWorld = SECURITY_WORLD_SID_AUTHORITY;
// Create a SID for the BUILTIN\Administrators group.
THROW_IF_WIN32_BOOL_FALSE(AllocateAndInitializeSid(&SIDAuthNT, 2, SECURITY_BUILTIN_DOMAIN_RID, DOMAIN_ALIAS_RID_ADMINS, 0, 0, 0, 0, 0, 0, &adminGroupSid));
// Create a well-known SID for the Everyone group.
THROW_IF_WIN32_BOOL_FALSE(AllocateAndInitializeSid(&SIDAuthWorld, 1, SECURITY_WORLD_RID, 0, 0, 0, 0, 0, 0, 0, &everyoneSid));
bool hadExpectedPermissions = true;
// Check that the permissions are what we'd expect them to be if only
// admins can write to the file. This is basically a mirror of what we
// set up in `WriteUTF8File`.
// For grfAccessPermissions, GENERIC_ALL turns into STANDARD_RIGHTS_ALL,
// and GENERIC_READ -> READ_CONTROL
hadExpectedPermissions &= WI_AreAllFlagsSet(pEA[0].grfAccessPermissions, STANDARD_RIGHTS_ALL);
hadExpectedPermissions &= pEA[0].grfInheritance == NO_INHERITANCE;
hadExpectedPermissions &= pEA[0].Trustee.TrusteeForm == TRUSTEE_IS_SID;
// SIDs are void*'s that happen to convert to a wchar_t
hadExpectedPermissions &= *(pEA[0].Trustee.ptstrName) == *(LPWSTR)(adminGroupSid.get());
// Now check the other EXPLICIT_ACCESS
hadExpectedPermissions &= WI_IsFlagSet(pEA[1].grfAccessPermissions, READ_CONTROL);
hadExpectedPermissions &= pEA[1].grfInheritance == NO_INHERITANCE;
hadExpectedPermissions &= pEA[1].Trustee.TrusteeForm == TRUSTEE_IS_SID;
hadExpectedPermissions &= *(pEA[1].Trustee.ptstrName) == *(LPWSTR)(everyoneSid.get());
return hadExpectedPermissions;
}
void WriteUTF8FileLocked(const locked_hfile& file, const std::string_view& content)
{
// truncate the file because we want to overwrite it
SetFilePointer(file.get(), 0, nullptr, FILE_BEGIN);
THROW_IF_WIN32_BOOL_FALSE(SetEndOfFile(file.get()));
const auto fileSize = gsl::narrow<DWORD>(content.size());
DWORD bytesWritten = 0;
THROW_IF_WIN32_BOOL_FALSE(WriteFile(file.get(), content.data(), fileSize, &bytesWritten, nullptr));
if (bytesWritten != fileSize)
{
THROW_WIN32_MSG(ERROR_WRITE_FAULT, "failed to write whole file");
}
}
// Tries to read a file somewhat atomically without locking it.
// Strips the UTF8 BOM if it exists.
std::string ReadUTF8File(const std::filesystem::path& path)
std::string ReadUTF8File(const std::filesystem::path& path, const bool elevatedOnly)
{
if (elevatedOnly)
{
const bool hadExpectedPermissions{ _hasExpectedPermissions(path) };
if (!hadExpectedPermissions)
{
// delete the file. It's been compromised.
LOG_LAST_ERROR_IF(!DeleteFile(path.c_str()));
// Exit early, because obviously there's nothing to read from the deleted file.
return "";
}
}
// From some casual observations we can determine that:
// * ReadFile() always returns the requested amount of data (unless the file is smaller)
// * It's unlikely that the file was changed between GetFileSize() and ReadFile()
@ -166,11 +170,11 @@ namespace winrt::Microsoft::Terminal::Settings::Model
}
// Same as ReadUTF8File, but returns an empty optional, if the file couldn't be opened.
std::optional<std::string> ReadUTF8FileIfExists(const std::filesystem::path& path)
std::optional<std::string> ReadUTF8FileIfExists(const std::filesystem::path& path, const bool elevatedOnly)
{
try
{
return { ReadUTF8File(path) };
return { ReadUTF8File(path, elevatedOnly) };
}
catch (const wil::ResultException& exception)
{
@ -183,9 +187,75 @@ namespace winrt::Microsoft::Terminal::Settings::Model
}
}
void WriteUTF8File(const std::filesystem::path& path, const std::string_view& content)
void WriteUTF8File(const std::filesystem::path& path,
const std::string_view& content,
const bool elevatedOnly)
{
wil::unique_hfile file{ CreateFileW(path.c_str(), GENERIC_WRITE, FILE_SHARE_READ | FILE_SHARE_WRITE, nullptr, CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL, nullptr) };
SECURITY_ATTRIBUTES sa;
if (elevatedOnly)
{
// This is very vaguely taken from
// https://docs.microsoft.com/en-us/windows/win32/secauthz/creating-a-security-descriptor-for-a-new-object-in-c--
// With using https://docs.microsoft.com/en-us/windows/win32/secauthz/well-known-sids
// to find out that
// * SECURITY_NT_AUTHORITY+SECURITY_LOCAL_SYSTEM_RID == NT AUTHORITY\SYSTEM
// * SECURITY_NT_AUTHORITY+SECURITY_BUILTIN_DOMAIN_RID+DOMAIN_ALIAS_RID_ADMINS == BUILTIN\Administrators
// * SECURITY_WORLD_SID_AUTHORITY+SECURITY_WORLD_RID == Everyone
//
// Raymond Chen recommended that I make this file only writable by
// SYSTEM, but if I did that, then even we can't write the file
// while elevated, which isn't what we want.
wil::unique_sid everyoneSid;
wil::unique_sid adminGroupSid;
SID_IDENTIFIER_AUTHORITY SIDAuthNT = SECURITY_NT_AUTHORITY;
SID_IDENTIFIER_AUTHORITY SIDAuthWorld = SECURITY_WORLD_SID_AUTHORITY;
// Create a SID for the BUILTIN\Administrators group.
THROW_IF_WIN32_BOOL_FALSE(AllocateAndInitializeSid(&SIDAuthNT, 2, SECURITY_BUILTIN_DOMAIN_RID, DOMAIN_ALIAS_RID_ADMINS, 0, 0, 0, 0, 0, 0, &adminGroupSid));
// Create a well-known SID for the Everyone group.
THROW_IF_WIN32_BOOL_FALSE(AllocateAndInitializeSid(&SIDAuthWorld, 1, SECURITY_WORLD_RID, 0, 0, 0, 0, 0, 0, 0, &everyoneSid));
EXPLICIT_ACCESS ea[2]{};
// Grant Admins all permissions on this file
ea[0].grfAccessPermissions = GENERIC_ALL;
ea[0].grfAccessMode = SET_ACCESS;
ea[0].grfInheritance = NO_INHERITANCE;
ea[0].Trustee.TrusteeForm = TRUSTEE_IS_SID;
ea[0].Trustee.TrusteeType = TRUSTEE_IS_WELL_KNOWN_GROUP;
ea[0].Trustee.ptstrName = (LPWSTR)(adminGroupSid.get());
// Grant Everyone the permission or read this file
ea[1].grfAccessPermissions = GENERIC_READ;
ea[1].grfAccessMode = SET_ACCESS;
ea[1].grfInheritance = NO_INHERITANCE;
ea[1].Trustee.TrusteeForm = TRUSTEE_IS_SID;
ea[1].Trustee.TrusteeType = TRUSTEE_IS_WELL_KNOWN_GROUP;
ea[1].Trustee.ptstrName = (LPWSTR)(everyoneSid.get());
ACL acl;
PACL pAcl = &acl;
THROW_IF_WIN32_ERROR(SetEntriesInAcl(2, ea, nullptr, &pAcl));
SECURITY_DESCRIPTOR sd;
THROW_IF_WIN32_BOOL_FALSE(InitializeSecurityDescriptor(&sd, SECURITY_DESCRIPTOR_REVISION));
THROW_IF_WIN32_BOOL_FALSE(SetSecurityDescriptorDacl(&sd, true, pAcl, false));
// Initialize a security attributes structure.
sa.nLength = sizeof(SECURITY_ATTRIBUTES);
sa.lpSecurityDescriptor = &sd;
sa.bInheritHandle = false;
}
wil::unique_hfile file{ CreateFileW(path.c_str(),
GENERIC_WRITE,
FILE_SHARE_READ | FILE_SHARE_WRITE,
elevatedOnly ? &sa : nullptr,
CREATE_ALWAYS,
FILE_ATTRIBUTE_NORMAL,
nullptr) };
THROW_LAST_ERROR_IF(!file);
const auto fileSize = gsl::narrow<DWORD>(content.size());
@ -198,7 +268,8 @@ namespace winrt::Microsoft::Terminal::Settings::Model
}
}
void WriteUTF8FileAtomic(const std::filesystem::path& path, const std::string_view& content)
void WriteUTF8FileAtomic(const std::filesystem::path& path,
const std::string_view& content)
{
// GH#10787: rename() will replace symbolic links themselves and not the path they point at.
// It's thus important that we first resolve them before generating temporary path.

View file

@ -1,41 +1,11 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT license.
#pragma once
namespace winrt::Microsoft::Terminal::Settings::Model
{
// I couldn't find a wil helper for this so I made it myself
class locked_hfile
{
public:
wil::unique_hfile file;
OVERLAPPED lockedRegion;
~locked_hfile()
{
if (file)
{
// Need to unlock the file before it is closed
UnlockFileEx(file.get(), 0, INT_MAX, 0, &lockedRegion);
}
}
HANDLE get() const noexcept
{
return file.get();
}
};
std::filesystem::path GetBaseSettingsPath();
locked_hfile OpenFileReadSharedLocked(const std::filesystem::path& path);
locked_hfile OpenFileRWExclusiveLocked(const std::filesystem::path& path);
std::string ReadUTF8FileLocked(const locked_hfile& file);
void WriteUTF8FileLocked(const locked_hfile& file, const std::string_view& content);
std::string ReadUTF8File(const std::filesystem::path& path);
std::optional<std::string> ReadUTF8FileIfExists(const std::filesystem::path& path);
void WriteUTF8File(const std::filesystem::path& path, const std::string_view& content);
std::string ReadUTF8File(const std::filesystem::path& path, const bool elevatedOnly = false);
std::optional<std::string> ReadUTF8FileIfExists(const std::filesystem::path& path, const bool elevatedOnly = false);
void WriteUTF8File(const std::filesystem::path& path, const std::string_view& content, const bool elevatedOnly = false);
void WriteUTF8FileAtomic(const std::filesystem::path& path, const std::string_view& content);
}

View file

@ -382,7 +382,6 @@ namespace Microsoft::Terminal::Settings::Model::JsonUtils
};
template<typename T>
struct ConversionTrait<std::unordered_map<std::string, T>>
{
std::unordered_map<std::string, T> FromJson(const Json::Value& json) const

View file

@ -37,7 +37,12 @@ std::wstring_view WslDistroGenerator::GetNamespace() const noexcept
static winrt::com_ptr<implementation::Profile> makeProfile(const std::wstring& distName)
{
const auto WSLDistro{ CreateDynamicProfile(distName) };
WSLDistro->Commandline(winrt::hstring{ L"wsl.exe -d " + distName });
// GH#11096 - make sure the WSL path starts explicitly with
// C:\Windows\System32. Don't want someone path hijacking wsl.exe.
wil::unique_cotaskmem_string systemPath;
THROW_IF_FAILED(wil::GetSystemDirectoryW(systemPath));
std::wstring command(systemPath.get());
WSLDistro->Commandline(winrt::hstring{ command + L"\\wsl.exe -d " + distName });
WSLDistro->DefaultAppearance().ColorSchemeName(L"Campbell");
WSLDistro->StartingDirectory(winrt::hstring{ DEFAULT_STARTING_DIRECTORY });
WSLDistro->Icon(L"ms-appx:///ProfileIcons/{9acb9455-ca41-5af7-950f-6bca1bc9722f}.png");

View file

@ -53,3 +53,6 @@ TRACELOGGING_DECLARE_PROVIDER(g_hSettingsModelProvider);
// Manually include til after we include Windows.Foundation to give it winrt superpowers
#include "til.h"
#include <til/mutex.h>
#include <til/throttled_func.h>

View file

@ -10,13 +10,13 @@
{
"guid": "{61c54bbd-c2c6-5271-96e7-009a87ff44bf}",
"name": "Windows PowerShell",
"commandline": "powershell.exe",
"commandline": "%SystemRoot%\\System32\\WindowsPowerShell\\v1.0\\powershell.exe",
"hidden": false
},
{
// "name" is filled in by CascadiaSettings as a localized string.
"guid": "{0caa0dad-35be-5f56-a8ff-afceeeaa6101}",
"commandline": "cmd.exe",
"commandline": "%SystemRoot%\\System32\\cmd.exe",
"hidden": false
}
]

View file

@ -819,8 +819,30 @@ winrt::Windows::Foundation::IAsyncAction AppHost::_SaveWindowLayouts()
if (_logic.ShouldUsePersistedLayout())
{
const auto layoutJsons = _windowManager.GetAllWindowLayouts();
_logic.SaveWindowLayoutJsons(layoutJsons);
try
{
TraceLoggingWrite(g_hWindowsTerminalProvider,
"AppHost_SaveWindowLayouts_Collect",
TraceLoggingDescription("Logged when collecting window state"),
TraceLoggingLevel(WINEVENT_LEVEL_VERBOSE),
TraceLoggingKeyword(TIL_KEYWORD_TRACE));
const auto layoutJsons = _windowManager.GetAllWindowLayouts();
TraceLoggingWrite(g_hWindowsTerminalProvider,
"AppHost_SaveWindowLayouts_Save",
TraceLoggingDescription("Logged when writing window state"),
TraceLoggingLevel(WINEVENT_LEVEL_VERBOSE),
TraceLoggingKeyword(TIL_KEYWORD_TRACE));
_logic.SaveWindowLayoutJsons(layoutJsons);
}
catch (...)
{
LOG_CAUGHT_EXCEPTION();
TraceLoggingWrite(g_hWindowsTerminalProvider,
"AppHost_SaveWindowLayouts_Failed",
TraceLoggingDescription("An error occured when collecting or writing window state"),
TraceLoggingLevel(WINEVENT_LEVEL_VERBOSE),
TraceLoggingKeyword(TIL_KEYWORD_TRACE));
}
}
co_return;
@ -841,6 +863,12 @@ winrt::fire_and_forget AppHost::_SaveWindowLayoutsRepeat()
// per 10 seconds, if a save is requested by another source simultaneously.
if (_getWindowLayoutThrottler.has_value())
{
TraceLoggingWrite(g_hWindowsTerminalProvider,
"AppHost_requestGetLayout",
TraceLoggingDescription("Logged when triggering a throttled write of the window state"),
TraceLoggingLevel(WINEVENT_LEVEL_VERBOSE),
TraceLoggingKeyword(TIL_KEYWORD_TRACE));
_getWindowLayoutThrottler.value()();
}
}

View file

@ -94,4 +94,5 @@ namespace Microsoft::Console::Utils
GUID CreateV5Uuid(const GUID& namespaceGuid, const gsl::span<const gsl::byte> name);
bool IsElevated();
}

View file

@ -5,6 +5,8 @@
#include "inc/utils.hpp"
#include "inc/colorTable.hpp"
#include <wil/token_helpers.h >
using namespace Microsoft::Console;
// Routine Description:
@ -559,3 +561,33 @@ GUID Utils::CreateV5Uuid(const GUID& namespaceGuid, const gsl::span<const gsl::b
::memcpy_s(&newGuid, sizeof(GUID), buffer.data(), sizeof(GUID));
return EndianSwap(newGuid);
}
bool Utils::IsElevated()
{
static bool isElevated = []() {
try
{
wil::unique_handle processToken{ GetCurrentProcessToken() };
const auto elevationType = wil::get_token_information<TOKEN_ELEVATION_TYPE>(processToken.get());
const auto elevationState = wil::get_token_information<TOKEN_ELEVATION>(processToken.get());
if (elevationType == TokenElevationTypeDefault && elevationState.TokenIsElevated)
{
// In this case, the user has UAC entirely disabled. This is sort of
// weird, we treat this like the user isn't an admin at all. There's no
// separation of powers, so the things we normally want to gate on
// "having special powers" doesn't apply.
//
// See GH#7754, GH#11096
return false;
}
return wil::test_token_membership(nullptr, SECURITY_NT_AUTHORITY, SECURITY_BUILTIN_DOMAIN_RID, DOMAIN_ALIAS_RID_ADMINS);
}
catch (...)
{
LOG_CAUGHT_EXCEPTION();
return false;
}
}();
return isElevated;
}