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,
static_cast<uint64_t>(_settingsLoadedResult),
_settingsLoadExceptionText,
warnings,
warnings.GetView(),
_settings);
SettingsChanged.raise(*this, *ev);
return;
@ -424,7 +424,7 @@ namespace winrt::TerminalApp::implementation
auto ev = winrt::make_self<SettingsLoadEventArgs>(!initialLoad,
_settingsLoadedResult,
_settingsLoadExceptionText,
warnings,
warnings.GetView(),
_settings);
SettingsChanged.raise(*this, *ev);
}
@ -491,7 +491,7 @@ namespace winrt::TerminalApp::implementation
auto ev = winrt::make_self<SettingsLoadEventArgs>(false,
_settingsLoadedResult,
_settingsLoadExceptionText,
warnings,
warnings.GetView(),
_settings);
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(uint64_t, Result, S_OK);
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);
public:
SettingsLoadEventArgs(bool reload,
uint64_t result,
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) :
_Reload{ reload },
_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;
}

View File

@ -187,6 +187,7 @@ namespace winrt::TerminalApp::implementation
til::typed_event<IInspectable, IInspectable> OpenSystemMenu;
til::typed_event<IInspectable, IInspectable> QuitRequested;
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::RequestReceiveContentArgs> RequestReceiveContent;

View File

@ -97,6 +97,7 @@ namespace TerminalApp
event Windows.Foundation.TypedEventHandler<Object, Object> OpenSystemMenu;
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, RequestReceiveContentArgs> RequestReceiveContent;

View File

@ -219,6 +219,7 @@ namespace winrt::TerminalApp::implementation
_root->Initialized({ get_weak(), &TerminalWindow::_pageInitialized });
_root->WindowSizeChanged({ get_weak(), &TerminalWindow::_WindowSizeChanged });
_root->RenameWindowRequested({ get_weak(), &TerminalWindow::_RenameWindowRequested });
_root->ShowLoadWarningsDialog({ get_weak(), &TerminalWindow::_ShowLoadWarningsDialog });
_root->Create();
AppLogic::Current()->SettingsChanged({ get_weak(), &TerminalWindow::UpdateSettingsHandler });
@ -476,7 +477,7 @@ namespace winrt::TerminalApp::implementation
// validating the settings.
// - Only one dialog can be visible at a time. If another dialog is visible
// 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 buttonText = RS_(L"Ok");
@ -536,7 +537,7 @@ namespace winrt::TerminalApp::implementation
}
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)
{
_ShowLoadWarningsDialog(args.Warnings());
_ShowLoadWarningsDialog(nullptr, args.Warnings());
}
else if (args.Result() == S_OK)
{

View File

@ -189,7 +189,7 @@ namespace winrt::TerminalApp::implementation
const winrt::hstring& contentKey,
HRESULT settingsLoadedResult,
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();

View File

@ -30,7 +30,7 @@ namespace TerminalApp
{
Boolean Reload { get; };
UInt64 Result { get; };
IVector<Microsoft.Terminal.Settings.Model.SettingsLoadWarnings> Warnings { get; };
IVectorView<Microsoft.Terminal.Settings.Model.SettingsLoadWarnings> Warnings { get; };
String ExceptionText { 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*/)
{
_settingsClone.LogSettingChanges(false);
_settingsClone.WriteSettingsToDisk();
if (!_settingsClone.WriteSettingsToDisk())
{
ShowLoadWarningsDialog.raise(*this, _settingsClone.Warnings());
}
}
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;
til::typed_event<Windows::Foundation::IInspectable, Model::SettingsTarget> OpenJson;
til::typed_event<Windows::Foundation::IInspectable, Windows::Foundation::Collections::IVectorView<Model::SettingsLoadWarnings>> ShowLoadWarningsDialog;
private:
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);
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.
// 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> ActiveProfiles() const noexcept;
Model::ActionMap ActionMap() const noexcept;
void WriteSettingsToDisk();
bool WriteSettingsToDisk();
Json::Value ToJson() const;
Model::Profile ProfileDefaults() const;
Model::Profile CreateNewProfile();

View File

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

View File

@ -1055,17 +1055,9 @@ try
// If we created the file, or found new dynamic profiles, write the user
// settings string back to the file.
if (mustWriteToDisk)
{
try
{
settings->WriteSettingsToDisk();
}
catch (...)
{
LOG_CAUGHT_EXCEPTION();
settings->_warnings.Append(SettingsLoadWarnings::FailedToWriteToSettings);
}
}
else
{
// lastWriteTime is only valid if mustWriteToDisk is false.
@ -1354,7 +1346,7 @@ winrt::hstring CascadiaSettings::DefaultSettingsPath()
// - <none>
// Return Value:
// - <none>
void CascadiaSettings::WriteSettingsToDisk()
bool CascadiaSettings::WriteSettingsToDisk()
{
const auto settingsPath = _settingsPath();
@ -1364,6 +1356,8 @@ void CascadiaSettings::WriteSettingsToDisk()
wbuilder.settings_["indentation"] = " ";
wbuilder.settings_["precision"] = 6; // prevent values like 1.1000000000000001
try
{
FILETIME lastWriteTime{};
const auto styledString{ Json::writeString(wbuilder, ToJson()) };
til::io::write_utf8_string_to_file_atomic(settingsPath, styledString, &lastWriteTime);
@ -1376,6 +1370,14 @@ void CascadiaSettings::WriteSettingsToDisk()
{
DefaultTerminal::Current(_currentDefaultTerminal);
}
}
catch (...)
{
LOG_CAUGHT_EXCEPTION();
_warnings.Append(SettingsLoadWarnings::FailedToWriteToSettings);
return false;
}
return true;
}
#ifndef NDEBUG

View File

@ -256,7 +256,11 @@ namespace til // Terminal Implementation Library. Also: "Today I Learned"
// renaming one is (supposed to be) atomic.
// Wait... "supposed to be"!? Well it's technically not always atomic,
// 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
} // til