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.

## Validation Steps Performed
-  Using SUI, save settings when the settings.json is set to read-only

Closes #18913
This commit is contained in:
Carlos Zamora 2025-06-24 13:06:03 -07:00 committed by GitHub
parent 3680e13bc0
commit 218c9fbe3e
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
15 changed files with 46 additions and 25 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

@ -220,6 +220,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 });
@ -478,7 +479,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");
@ -539,7 +540,7 @@ namespace winrt::TerminalApp::implementation
}
else if (settingsLoadedResult == S_FALSE)
{
_ShowLoadWarningsDialog(_initialLoadResult.Warnings());
_ShowLoadWarningsDialog(nullptr, _initialLoadResult.Warnings());
}
}
@ -825,7 +826,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

@ -680,7 +680,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

@ -46,6 +46,7 @@ namespace winrt::Microsoft::Terminal::Settings::Editor::implementation
Editor::ExtensionsViewModel ExtensionsVM() const noexcept { return _extensionsVM; }
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

@ -39,6 +39,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

@ -167,7 +167,7 @@ namespace winrt::Microsoft::Terminal::Settings::Model::implementation
winrt::Windows::Foundation::Collections::IVectorView<Model::ExtensionPackage> Extensions();
void ResetApplicationState() const;
void ResetToDefaultSettings();
void WriteSettingsToDisk();
bool WriteSettingsToDisk();
Json::Value ToJson() const;
Model::Profile ProfileDefaults() const;
Model::Profile CreateNewProfile();

View File

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

View File

@ -1225,17 +1225,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.
@ -1539,7 +1531,7 @@ void CascadiaSettings::ResetToDefaultSettings()
// - <none>
// Return Value:
// - <none>
void CascadiaSettings::WriteSettingsToDisk()
bool CascadiaSettings::WriteSettingsToDisk()
{
// write current settings to current settings file
Json::StreamWriterBuilder wbuilder;
@ -1547,7 +1539,17 @@ void CascadiaSettings::WriteSettingsToDisk()
wbuilder.settings_["indentation"] = " ";
wbuilder.settings_["precision"] = 6; // prevent values like 1.1000000000000001
try
{
_writeSettingsToDisk(Json::writeString(wbuilder, ToJson()));
}
catch (...)
{
LOG_CAUGHT_EXCEPTION();
_warnings.Append(SettingsLoadWarnings::FailedToWriteToSettings);
return false;
}
return true;
}
void CascadiaSettings::_writeSettingsToDisk(std::string_view contents)

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