Display a warning if SUI is unable to write to the settings file (#19027)

Adds logic to display a warning popup if the settings.json is marked as
read-only and we try to write to the settings.json file. Previously,
this scenario would crash, which definitely isn't right. However, a
simple fix of "not-crashing" wouldn't feel right either.

This leverages the existing infrastructure to display a warning dialog
when we failed to write to the settings file. The main annoyance here is
that that popup dialog is located in `TerminalWindow` and is normally
triggered from a failed `SettingsLoadEventArgs`. To get around this,
`CascadiaSettings::WriteSettingsToDisk()` now returns a boolean to
signal if the write was successful; whereas if it fails, a warning is
added to the settings object. If we fail to write to disk, the function
will return false and we'll raise an event with the settings' warnings
to `TerminalPage` which passes it along to `TerminalWindow`.

Additionally, this uses `IVectorView<SettingsLoadWarnings>` as opposed
to `IVector<SettingsLoadWarnings>` throughout the relevant code. It's
more correct as the list of warnings shouldn't be mutable and the
warnings from the `CascadiaSettings` object are retrieved in that
format.

-  Using SUI, save settings when the settings.json is set to read-only

Closes #18913

(cherry picked from commit 218c9fbe3ecd79cc15d9cab267753b1b68ead250)
Service-Card-Id: PVTI_lADOAF3p4s4Axadtzgb3QSk
Service-Version: 1.23
This commit is contained in:
Carlos Zamora 2025-06-24 13:06:03 -07:00 committed by Dustin L. Howett
parent 820b10fd2b
commit 38f28e80d0
15 changed files with 55 additions and 34 deletions

View File

@ -392,7 +392,7 @@ namespace winrt::TerminalApp::implementation
auto ev = winrt::make_self<SettingsLoadEventArgs>(true, auto ev = winrt::make_self<SettingsLoadEventArgs>(true,
static_cast<uint64_t>(_settingsLoadedResult), static_cast<uint64_t>(_settingsLoadedResult),
_settingsLoadExceptionText, _settingsLoadExceptionText,
warnings, warnings.GetView(),
_settings); _settings);
SettingsChanged.raise(*this, *ev); SettingsChanged.raise(*this, *ev);
return; return;
@ -424,7 +424,7 @@ namespace winrt::TerminalApp::implementation
auto ev = winrt::make_self<SettingsLoadEventArgs>(!initialLoad, auto ev = winrt::make_self<SettingsLoadEventArgs>(!initialLoad,
_settingsLoadedResult, _settingsLoadedResult,
_settingsLoadExceptionText, _settingsLoadExceptionText,
warnings, warnings.GetView(),
_settings); _settings);
SettingsChanged.raise(*this, *ev); SettingsChanged.raise(*this, *ev);
} }
@ -491,7 +491,7 @@ namespace winrt::TerminalApp::implementation
auto ev = winrt::make_self<SettingsLoadEventArgs>(false, auto ev = winrt::make_self<SettingsLoadEventArgs>(false,
_settingsLoadedResult, _settingsLoadedResult,
_settingsLoadExceptionText, _settingsLoadExceptionText,
warnings, warnings.GetView(),
_settings); _settings);
auto window = winrt::make_self<implementation::TerminalWindow>(*ev, _contentManager); auto window = winrt::make_self<implementation::TerminalWindow>(*ev, _contentManager);

View File

@ -12,14 +12,14 @@ namespace winrt::TerminalApp::implementation
WINRT_PROPERTY(bool, Reload, false); WINRT_PROPERTY(bool, Reload, false);
WINRT_PROPERTY(uint64_t, Result, S_OK); WINRT_PROPERTY(uint64_t, Result, S_OK);
WINRT_PROPERTY(winrt::hstring, ExceptionText, L""); WINRT_PROPERTY(winrt::hstring, ExceptionText, L"");
WINRT_PROPERTY(winrt::Windows::Foundation::Collections::IVector<Microsoft::Terminal::Settings::Model::SettingsLoadWarnings>, Warnings, nullptr); WINRT_PROPERTY(winrt::Windows::Foundation::Collections::IVectorView<Microsoft::Terminal::Settings::Model::SettingsLoadWarnings>, Warnings, nullptr);
WINRT_PROPERTY(Microsoft::Terminal::Settings::Model::CascadiaSettings, NewSettings, nullptr); WINRT_PROPERTY(Microsoft::Terminal::Settings::Model::CascadiaSettings, NewSettings, nullptr);
public: public:
SettingsLoadEventArgs(bool reload, SettingsLoadEventArgs(bool reload,
uint64_t result, uint64_t result,
winrt::hstring exceptionText, winrt::hstring exceptionText,
winrt::Windows::Foundation::Collections::IVector<Microsoft::Terminal::Settings::Model::SettingsLoadWarnings> warnings, winrt::Windows::Foundation::Collections::IVectorView<Microsoft::Terminal::Settings::Model::SettingsLoadWarnings> warnings,
Microsoft::Terminal::Settings::Model::CascadiaSettings newSettings) : Microsoft::Terminal::Settings::Model::CascadiaSettings newSettings) :
_Reload{ reload }, _Reload{ reload },
_Result{ result }, _Result{ result },

View File

@ -4165,6 +4165,13 @@ namespace winrt::TerminalApp::implementation
} }
}); });
sui.ShowLoadWarningsDialog([weakThis{ get_weak() }](auto&& /*s*/, const Windows::Foundation::Collections::IVectorView<winrt::Microsoft::Terminal::Settings::Model::SettingsLoadWarnings>& warnings) {
if (auto page{ weakThis.get() })
{
page->ShowLoadWarningsDialog.raise(*page, warnings);
}
});
return *settingsContent; return *settingsContent;
} }

View File

@ -187,6 +187,7 @@ namespace winrt::TerminalApp::implementation
til::typed_event<IInspectable, IInspectable> OpenSystemMenu; til::typed_event<IInspectable, IInspectable> OpenSystemMenu;
til::typed_event<IInspectable, IInspectable> QuitRequested; til::typed_event<IInspectable, IInspectable> QuitRequested;
til::typed_event<IInspectable, winrt::Microsoft::Terminal::Control::ShowWindowArgs> ShowWindowChanged; til::typed_event<IInspectable, winrt::Microsoft::Terminal::Control::ShowWindowArgs> ShowWindowChanged;
til::typed_event<Windows::Foundation::IInspectable, Windows::Foundation::Collections::IVectorView<winrt::Microsoft::Terminal::Settings::Model::SettingsLoadWarnings>> ShowLoadWarningsDialog;
til::typed_event<Windows::Foundation::IInspectable, winrt::TerminalApp::RequestMoveContentArgs> RequestMoveContent; til::typed_event<Windows::Foundation::IInspectable, winrt::TerminalApp::RequestMoveContentArgs> RequestMoveContent;
til::typed_event<Windows::Foundation::IInspectable, winrt::TerminalApp::RequestReceiveContentArgs> RequestReceiveContent; til::typed_event<Windows::Foundation::IInspectable, winrt::TerminalApp::RequestReceiveContentArgs> RequestReceiveContent;

View File

@ -97,6 +97,7 @@ namespace TerminalApp
event Windows.Foundation.TypedEventHandler<Object, Object> OpenSystemMenu; event Windows.Foundation.TypedEventHandler<Object, Object> OpenSystemMenu;
event Windows.Foundation.TypedEventHandler<Object, Microsoft.Terminal.Control.ShowWindowArgs> ShowWindowChanged; event Windows.Foundation.TypedEventHandler<Object, Microsoft.Terminal.Control.ShowWindowArgs> ShowWindowChanged;
event Windows.Foundation.TypedEventHandler<Object, Windows.Foundation.Collections.IVectorView<Microsoft.Terminal.Settings.Model.SettingsLoadWarnings> > ShowLoadWarningsDialog;
event Windows.Foundation.TypedEventHandler<Object, RequestMoveContentArgs> RequestMoveContent; event Windows.Foundation.TypedEventHandler<Object, RequestMoveContentArgs> RequestMoveContent;
event Windows.Foundation.TypedEventHandler<Object, RequestReceiveContentArgs> RequestReceiveContent; event Windows.Foundation.TypedEventHandler<Object, RequestReceiveContentArgs> RequestReceiveContent;

View File

@ -219,6 +219,7 @@ namespace winrt::TerminalApp::implementation
_root->Initialized({ get_weak(), &TerminalWindow::_pageInitialized }); _root->Initialized({ get_weak(), &TerminalWindow::_pageInitialized });
_root->WindowSizeChanged({ get_weak(), &TerminalWindow::_WindowSizeChanged }); _root->WindowSizeChanged({ get_weak(), &TerminalWindow::_WindowSizeChanged });
_root->RenameWindowRequested({ get_weak(), &TerminalWindow::_RenameWindowRequested }); _root->RenameWindowRequested({ get_weak(), &TerminalWindow::_RenameWindowRequested });
_root->ShowLoadWarningsDialog({ get_weak(), &TerminalWindow::_ShowLoadWarningsDialog });
_root->Create(); _root->Create();
AppLogic::Current()->SettingsChanged({ get_weak(), &TerminalWindow::UpdateSettingsHandler }); AppLogic::Current()->SettingsChanged({ get_weak(), &TerminalWindow::UpdateSettingsHandler });
@ -476,7 +477,7 @@ namespace winrt::TerminalApp::implementation
// validating the settings. // validating the settings.
// - Only one dialog can be visible at a time. If another dialog is visible // - Only one dialog can be visible at a time. If another dialog is visible
// when this is called, nothing happens. See ShowDialog for details // when this is called, nothing happens. See ShowDialog for details
void TerminalWindow::_ShowLoadWarningsDialog(const Windows::Foundation::Collections::IVector<SettingsLoadWarnings>& warnings) void TerminalWindow::_ShowLoadWarningsDialog(const IInspectable&, const Windows::Foundation::Collections::IVectorView<SettingsLoadWarnings>& warnings)
{ {
auto title = RS_(L"SettingsValidateErrorTitle"); auto title = RS_(L"SettingsValidateErrorTitle");
auto buttonText = RS_(L"Ok"); auto buttonText = RS_(L"Ok");
@ -536,7 +537,7 @@ namespace winrt::TerminalApp::implementation
} }
else if (settingsLoadedResult == S_FALSE) else if (settingsLoadedResult == S_FALSE)
{ {
_ShowLoadWarningsDialog(_initialLoadResult.Warnings()); _ShowLoadWarningsDialog(nullptr, _initialLoadResult.Warnings());
} }
} }
@ -822,7 +823,7 @@ namespace winrt::TerminalApp::implementation
} }
else if (args.Result() == S_FALSE) else if (args.Result() == S_FALSE)
{ {
_ShowLoadWarningsDialog(args.Warnings()); _ShowLoadWarningsDialog(nullptr, args.Warnings());
} }
else if (args.Result() == S_OK) else if (args.Result() == S_OK)
{ {

View File

@ -189,7 +189,7 @@ namespace winrt::TerminalApp::implementation
const winrt::hstring& contentKey, const winrt::hstring& contentKey,
HRESULT settingsLoadedResult, HRESULT settingsLoadedResult,
const winrt::hstring& exceptionText); const winrt::hstring& exceptionText);
void _ShowLoadWarningsDialog(const Windows::Foundation::Collections::IVector<Microsoft::Terminal::Settings::Model::SettingsLoadWarnings>& warnings); void _ShowLoadWarningsDialog(const IInspectable& sender, const Windows::Foundation::Collections::IVectorView<Microsoft::Terminal::Settings::Model::SettingsLoadWarnings>& warnings);
bool _IsKeyboardServiceEnabled(); bool _IsKeyboardServiceEnabled();

View File

@ -30,7 +30,7 @@ namespace TerminalApp
{ {
Boolean Reload { get; }; Boolean Reload { get; };
UInt64 Result { get; }; UInt64 Result { get; };
IVector<Microsoft.Terminal.Settings.Model.SettingsLoadWarnings> Warnings { get; }; IVectorView<Microsoft.Terminal.Settings.Model.SettingsLoadWarnings> Warnings { get; };
String ExceptionText { get; }; String ExceptionText { get; };
Microsoft.Terminal.Settings.Model.CascadiaSettings NewSettings { get; }; Microsoft.Terminal.Settings.Model.CascadiaSettings NewSettings { get; };

View File

@ -590,7 +590,10 @@ namespace winrt::Microsoft::Terminal::Settings::Editor::implementation
void MainPage::SaveButton_Click(const IInspectable& /*sender*/, const RoutedEventArgs& /*args*/) void MainPage::SaveButton_Click(const IInspectable& /*sender*/, const RoutedEventArgs& /*args*/)
{ {
_settingsClone.LogSettingChanges(false); _settingsClone.LogSettingChanges(false);
_settingsClone.WriteSettingsToDisk(); if (!_settingsClone.WriteSettingsToDisk())
{
ShowLoadWarningsDialog.raise(*this, _settingsClone.Warnings());
}
} }
void MainPage::ResetButton_Click(const IInspectable& /*sender*/, const RoutedEventArgs& /*args*/) void MainPage::ResetButton_Click(const IInspectable& /*sender*/, const RoutedEventArgs& /*args*/)

View File

@ -45,6 +45,7 @@ namespace winrt::Microsoft::Terminal::Settings::Editor::implementation
Windows::Foundation::Collections::IObservableVector<IInspectable> Breadcrumbs() noexcept; Windows::Foundation::Collections::IObservableVector<IInspectable> Breadcrumbs() noexcept;
til::typed_event<Windows::Foundation::IInspectable, Model::SettingsTarget> OpenJson; til::typed_event<Windows::Foundation::IInspectable, Model::SettingsTarget> OpenJson;
til::typed_event<Windows::Foundation::IInspectable, Windows::Foundation::Collections::IVectorView<Model::SettingsLoadWarnings>> ShowLoadWarningsDialog;
private: private:
Windows::Foundation::Collections::IObservableVector<IInspectable> _breadcrumbs; Windows::Foundation::Collections::IObservableVector<IInspectable> _breadcrumbs;

View File

@ -36,6 +36,7 @@ namespace Microsoft.Terminal.Settings.Editor
void UpdateSettings(Microsoft.Terminal.Settings.Model.CascadiaSettings settings); void UpdateSettings(Microsoft.Terminal.Settings.Model.CascadiaSettings settings);
event Windows.Foundation.TypedEventHandler<Object, Microsoft.Terminal.Settings.Model.SettingsTarget> OpenJson; event Windows.Foundation.TypedEventHandler<Object, Microsoft.Terminal.Settings.Model.SettingsTarget> OpenJson;
event Windows.Foundation.TypedEventHandler<Object, Windows.Foundation.Collections.IVectorView<Microsoft.Terminal.Settings.Model.SettingsLoadWarnings> > ShowLoadWarningsDialog;
// Due to the aforementioned bug, we can't use IInitializeWithWindow _here_ either. // Due to the aforementioned bug, we can't use IInitializeWithWindow _here_ either.
// Let's just smuggle the HWND in as a UInt64 :| // Let's just smuggle the HWND in as a UInt64 :|

View File

@ -127,7 +127,7 @@ namespace winrt::Microsoft::Terminal::Settings::Model::implementation
winrt::Windows::Foundation::Collections::IObservableVector<Model::Profile> AllProfiles() const noexcept; winrt::Windows::Foundation::Collections::IObservableVector<Model::Profile> AllProfiles() const noexcept;
winrt::Windows::Foundation::Collections::IObservableVector<Model::Profile> ActiveProfiles() const noexcept; winrt::Windows::Foundation::Collections::IObservableVector<Model::Profile> ActiveProfiles() const noexcept;
Model::ActionMap ActionMap() const noexcept; Model::ActionMap ActionMap() const noexcept;
void WriteSettingsToDisk(); bool WriteSettingsToDisk();
Json::Value ToJson() const; Json::Value ToJson() const;
Model::Profile ProfileDefaults() const; Model::Profile ProfileDefaults() const;
Model::Profile CreateNewProfile(); Model::Profile CreateNewProfile();

View File

@ -23,7 +23,7 @@ namespace Microsoft.Terminal.Settings.Model
CascadiaSettings(String userJSON, String inboxJSON); CascadiaSettings(String userJSON, String inboxJSON);
CascadiaSettings Copy(); CascadiaSettings Copy();
void WriteSettingsToDisk(); Boolean WriteSettingsToDisk();
void LogSettingChanges(Boolean isJsonLoad); void LogSettingChanges(Boolean isJsonLoad);
String Hash { get; }; String Hash { get; };

View File

@ -1056,15 +1056,7 @@ try
// settings string back to the file. // settings string back to the file.
if (mustWriteToDisk) if (mustWriteToDisk)
{ {
try settings->WriteSettingsToDisk();
{
settings->WriteSettingsToDisk();
}
catch (...)
{
LOG_CAUGHT_EXCEPTION();
settings->_warnings.Append(SettingsLoadWarnings::FailedToWriteToSettings);
}
} }
else else
{ {
@ -1354,7 +1346,7 @@ winrt::hstring CascadiaSettings::DefaultSettingsPath()
// - <none> // - <none>
// Return Value: // Return Value:
// - <none> // - <none>
void CascadiaSettings::WriteSettingsToDisk() bool CascadiaSettings::WriteSettingsToDisk()
{ {
const auto settingsPath = _settingsPath(); const auto settingsPath = _settingsPath();
@ -1364,18 +1356,28 @@ void CascadiaSettings::WriteSettingsToDisk()
wbuilder.settings_["indentation"] = " "; wbuilder.settings_["indentation"] = " ";
wbuilder.settings_["precision"] = 6; // prevent values like 1.1000000000000001 wbuilder.settings_["precision"] = 6; // prevent values like 1.1000000000000001
FILETIME lastWriteTime{}; try
const auto styledString{ Json::writeString(wbuilder, ToJson()) };
til::io::write_utf8_string_to_file_atomic(settingsPath, styledString, &lastWriteTime);
_hash = _calculateHash(styledString, lastWriteTime);
// Persists the default terminal choice
// GH#10003 - Only do this if _currentDefaultTerminal was actually initialized.
if (_currentDefaultTerminal)
{ {
DefaultTerminal::Current(_currentDefaultTerminal); FILETIME lastWriteTime{};
const auto styledString{ Json::writeString(wbuilder, ToJson()) };
til::io::write_utf8_string_to_file_atomic(settingsPath, styledString, &lastWriteTime);
_hash = _calculateHash(styledString, lastWriteTime);
// Persists the default terminal choice
// GH#10003 - Only do this if _currentDefaultTerminal was actually initialized.
if (_currentDefaultTerminal)
{
DefaultTerminal::Current(_currentDefaultTerminal);
}
} }
catch (...)
{
LOG_CAUGHT_EXCEPTION();
_warnings.Append(SettingsLoadWarnings::FailedToWriteToSettings);
return false;
}
return true;
} }
#ifndef NDEBUG #ifndef NDEBUG

View File

@ -256,7 +256,11 @@ namespace til // Terminal Implementation Library. Also: "Today I Learned"
// renaming one is (supposed to be) atomic. // renaming one is (supposed to be) atomic.
// Wait... "supposed to be"!? Well it's technically not always atomic, // Wait... "supposed to be"!? Well it's technically not always atomic,
// but it's pretty darn close to it, so... better than nothing. // but it's pretty darn close to it, so... better than nothing.
std::filesystem::rename(tmpPath, resolvedPath); std::filesystem::rename(tmpPath, resolvedPath, ec);
if (ec)
{
THROW_WIN32_MSG(ec.value(), "failed to write to file");
}
} }
} // io } // io
} // til } // til