diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-19 00:47:55 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-19 00:47:55 +0000 |
commit | 26a029d407be480d791972afb5975cf62c9360a6 (patch) | |
tree | f435a8308119effd964b339f76abb83a57c29483 /widget/windows/filedialog | |
parent | Initial commit. (diff) | |
download | firefox-26a029d407be480d791972afb5975cf62c9360a6.tar.xz firefox-26a029d407be480d791972afb5975cf62c9360a6.zip |
Adding upstream version 124.0.1.upstream/124.0.1
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'widget/windows/filedialog')
-rw-r--r-- | widget/windows/filedialog/PWinFileDialog.ipdl | 32 | ||||
-rw-r--r-- | widget/windows/filedialog/WinFileDialogChild.cpp | 110 | ||||
-rw-r--r-- | widget/windows/filedialog/WinFileDialogChild.h | 52 | ||||
-rw-r--r-- | widget/windows/filedialog/WinFileDialogCommands.cpp | 460 | ||||
-rw-r--r-- | widget/windows/filedialog/WinFileDialogCommands.h | 74 | ||||
-rw-r--r-- | widget/windows/filedialog/WinFileDialogCommandsDefn.ipdlh | 49 | ||||
-rw-r--r-- | widget/windows/filedialog/WinFileDialogParent.cpp | 94 | ||||
-rw-r--r-- | widget/windows/filedialog/WinFileDialogParent.h | 90 | ||||
-rw-r--r-- | widget/windows/filedialog/moz.build | 27 |
9 files changed, 988 insertions, 0 deletions
diff --git a/widget/windows/filedialog/PWinFileDialog.ipdl b/widget/windows/filedialog/PWinFileDialog.ipdl new file mode 100644 index 0000000000..812db7e103 --- /dev/null +++ b/widget/windows/filedialog/PWinFileDialog.ipdl @@ -0,0 +1,32 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set sw=2 ts=8 et ft=ipdl : */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +include WinFileDialogCommandsDefn; +using mozilla::WindowsHandle from "mozilla/ipc/IPCTypes.h"; +using mozilla::widget::filedialog::FileDialogType from "mozilla/widget/filedialog/WinFileDialogCommands.h"; + +namespace mozilla { +namespace widget { +namespace filedialog { + +[ChildProc=Utility] +protocol PWinFileDialog { + +child: + // Exactly one Show function should be called per instance. Further calls will + // result in IPC failure. + // + // Each will return `Nothing` iff the operation was canceled by the user. + + async ShowFileDialog(WindowsHandle parentHwnd, FileDialogType type, Command[] commands) + returns (Results? results); + async ShowFolderDialog(WindowsHandle parentHwnd, Command[] commands) + returns (nsString? path); +}; + +} // namespace filedialog +} // namespace widget +} // namespace mozilla diff --git a/widget/windows/filedialog/WinFileDialogChild.cpp b/widget/windows/filedialog/WinFileDialogChild.cpp new file mode 100644 index 0000000000..1a2903f8ec --- /dev/null +++ b/widget/windows/filedialog/WinFileDialogChild.cpp @@ -0,0 +1,110 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "mozilla/widget/filedialog/WinFileDialogChild.h" + +#include <combaseapi.h> +#include <objbase.h> +#include <shobjidl.h> + +#include "mozilla/Assertions.h" +#include "mozilla/ipc/ProtocolUtils.h" +#include "mozilla/widget/filedialog/WinFileDialogCommands.h" +#include "nsPrintfCString.h" + +namespace mozilla::widget::filedialog { + +/* extern */ mozilla::LazyLogModule sLogFileDialog("FileDialog"); + +WinFileDialogChild::WinFileDialogChild() { + MOZ_LOG(sLogFileDialog, LogLevel::Info, ("%s %p", __PRETTY_FUNCTION__, this)); +}; + +WinFileDialogChild::~WinFileDialogChild() { + MOZ_LOG(sLogFileDialog, LogLevel::Info, ("%s %p", __PRETTY_FUNCTION__, this)); +}; + +#define MOZ_ABORT_IF_ALREADY_USED() \ + do { \ + MOZ_RELEASE_ASSERT( \ + !mUsed, "called Show* twice on a single WinFileDialog instance"); \ + MOZ_LOG( \ + sLogFileDialog, LogLevel::Info, \ + ("%s %p: first call to a Show* function", __PRETTY_FUNCTION__, this)); \ + mUsed = true; \ + } while (0) + +template <size_t N> +WinFileDialogChild::IPCResult WinFileDialogChild::MakeIpcFailure( + HRESULT hr, const char (&what)[N]) { + // The crash-report annotator stringifies integer values anyway. We do so + // eagerly here to avoid questions about C int/long conversion semantics. + nsPrintfCString data("%lu", hr); + CrashReporter::AnnotateCrashReport( + CrashReporter::Annotation::WindowsFileDialogErrorCode, data); + + return IPC_FAIL(this, what); +} + +#define MOZ_IPC_ENSURE_HRESULT_OK(hr, what) \ + do { \ + MOZ_LOG(sLogFileDialog, LogLevel::Verbose, \ + ("checking HRESULT for %s", what)); \ + HRESULT const _hr_ = (hr); \ + if (FAILED(_hr_)) { \ + MOZ_LOG(sLogFileDialog, LogLevel::Error, \ + ("HRESULT %8lX while %s", (hr), (what))); \ + return MakeIpcFailure(_hr_, (what)); \ + } \ + } while (0) + +WinFileDialogChild::IPCResult WinFileDialogChild::RecvShowFileDialog( + uintptr_t parentHwnd, FileDialogType type, nsTArray<Command> commands, + FileResolver&& resolver) { + MOZ_ABORT_IF_ALREADY_USED(); + + SpawnFilePicker(HWND(parentHwnd), type, std::move(commands)) + ->Then( + GetMainThreadSerialEventTarget(), __PRETTY_FUNCTION__, + [resolver = std::move(resolver)](Maybe<Results> const& res) { + resolver(res); + }, + [self = RefPtr(this)](HRESULT hr) { + // this doesn't need to be returned anywhere; it'll crash the + // process as a side effect of construction + self->MakeIpcFailure(hr, "SpawnFilePicker"); + }); + + return IPC_OK(); +} + +WinFileDialogChild::IPCResult WinFileDialogChild::RecvShowFolderDialog( + uintptr_t parentHwnd, nsTArray<Command> commands, + FolderResolver&& resolver) { + MOZ_ABORT_IF_ALREADY_USED(); + + SpawnFolderPicker(HWND(parentHwnd), std::move(commands)) + ->Then( + GetMainThreadSerialEventTarget(), __PRETTY_FUNCTION__, + [resolver = std::move(resolver)](Maybe<nsString> const& res) { + resolver(res); + }, + [self = RefPtr(this), resolver](HRESULT hr) { + // this doesn't need to be returned anywhere; it'll crash the + // process as a side effect of construction + self->MakeIpcFailure(hr, "SpawnFolderPicker"); + }); + + return IPC_OK(); +} + +#undef MOZ_IPC_ENSURE_HRESULT_OK + +void WinFileDialogChild::ProcessingError(Result aCode, const char* aReason) { + detail::LogProcessingError(sLogFileDialog, this, aCode, aReason); +} + +} // namespace mozilla::widget::filedialog diff --git a/widget/windows/filedialog/WinFileDialogChild.h b/widget/windows/filedialog/WinFileDialogChild.h new file mode 100644 index 0000000000..b0939ce2ed --- /dev/null +++ b/widget/windows/filedialog/WinFileDialogChild.h @@ -0,0 +1,52 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef widget_windows_filedialog_WinFileDialogChild_h__ +#define widget_windows_filedialog_WinFileDialogChild_h__ + +#include "mozilla/widget/filedialog/PWinFileDialogChild.h" + +// forward declaration of native Windows interface-struct +struct IFileDialog; + +namespace mozilla::widget::filedialog { + +class WinFileDialogChild : public PWinFileDialogChild { + public: + using Command = mozilla::widget::filedialog::Command; + using IPCResult = mozilla::ipc::IPCResult; + NS_INLINE_DECL_THREADSAFE_REFCOUNTING(WinFileDialogChild, override); + + WinFileDialogChild(); + + public: + using FileResolver = PWinFileDialogChild::ShowFileDialogResolver; + IPCResult RecvShowFileDialog(uintptr_t parentHwnd, FileDialogType, + nsTArray<Command>, FileResolver&&); + + using FolderResolver = PWinFileDialogChild::ShowFolderDialogResolver; + IPCResult RecvShowFolderDialog(uintptr_t parentHwnd, nsTArray<Command>, + FolderResolver&&); + + private: + ~WinFileDialogChild(); + + void ProcessingError(Result aCode, const char* aReason) override; + + // Defined and used only in WinFileDialogChild.cpp. + template <size_t N> + IPCResult MakeIpcFailure(HRESULT hr, const char (&what)[N]); + + // This flag properly _should_ be static (_i.e._, per-process) rather than + // per-instance; but we can't presently instantiate two separate utility + // processes with the same sandbox type, so we have to reuse the existing + // utility process if there is one. + bool mUsed = false; +}; + +} // namespace mozilla::widget::filedialog + +#endif // widget_windows_filedialog_WinFileDialogChild_h__ diff --git a/widget/windows/filedialog/WinFileDialogCommands.cpp b/widget/windows/filedialog/WinFileDialogCommands.cpp new file mode 100644 index 0000000000..f0503ab8f0 --- /dev/null +++ b/widget/windows/filedialog/WinFileDialogCommands.cpp @@ -0,0 +1,460 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "mozilla/widget/filedialog/WinFileDialogCommands.h" + +#include <type_traits> +#include <shobjidl.h> +#include <shtypes.h> +#include <winerror.h> +#include "WinUtils.h" +#include "mozilla/Logging.h" +#include "mozilla/RefPtr.h" +#include "mozilla/UniquePtrExtensions.h" +#include "mozilla/WinHeaderOnlyUtils.h" +#include "mozilla/ipc/ProtocolUtils.h" +#include "mozilla/ipc/UtilityProcessManager.h" +#include "mozilla/mscom/ApartmentRegion.h" +#include "nsThreadUtils.h" + +namespace mozilla::widget::filedialog { + +// Visitor to apply commands to the dialog. +struct Applicator { + IFileDialog* dialog = nullptr; + + HRESULT Visit(Command const& c) { + switch (c.type()) { + default: + case Command::T__None: + return E_INVALIDARG; + + case Command::TSetOptions: + return Apply(c.get_SetOptions()); + case Command::TSetTitle: + return Apply(c.get_SetTitle()); + case Command::TSetOkButtonLabel: + return Apply(c.get_SetOkButtonLabel()); + case Command::TSetFolder: + return Apply(c.get_SetFolder()); + case Command::TSetFileName: + return Apply(c.get_SetFileName()); + case Command::TSetDefaultExtension: + return Apply(c.get_SetDefaultExtension()); + case Command::TSetFileTypes: + return Apply(c.get_SetFileTypes()); + case Command::TSetFileTypeIndex: + return Apply(c.get_SetFileTypeIndex()); + } + } + + HRESULT Apply(SetOptions const& c) { return dialog->SetOptions(c.options()); } + HRESULT Apply(SetTitle const& c) { return dialog->SetTitle(c.title().get()); } + HRESULT Apply(SetOkButtonLabel const& c) { + return dialog->SetOkButtonLabel(c.label().get()); + } + HRESULT Apply(SetFolder const& c) { + RefPtr<IShellItem> folder; + if (SUCCEEDED(SHCreateItemFromParsingName( + c.path().get(), nullptr, IID_IShellItem, getter_AddRefs(folder)))) { + return dialog->SetFolder(folder); + } + // graciously accept that the provided path may have been nonsense + return S_OK; + } + HRESULT Apply(SetFileName const& c) { + return dialog->SetFileName(c.filename().get()); + } + HRESULT Apply(SetDefaultExtension const& c) { + return dialog->SetDefaultExtension(c.extension().get()); + } + HRESULT Apply(SetFileTypes const& c) { + std::vector<COMDLG_FILTERSPEC> vec; + for (auto const& filter : c.filterList()) { + vec.push_back( + {.pszName = filter.name().get(), .pszSpec = filter.spec().get()}); + } + return dialog->SetFileTypes(vec.size(), vec.data()); + } + HRESULT Apply(SetFileTypeIndex const& c) { + return dialog->SetFileTypeIndex(c.index()); + } +}; + +namespace { +static HRESULT GetShellItemPath(IShellItem* aItem, nsString& aResultString) { + NS_ENSURE_TRUE(aItem, E_INVALIDARG); + + mozilla::UniquePtr<wchar_t, CoTaskMemFreeDeleter> str; + HRESULT const hr = + aItem->GetDisplayName(SIGDN_FILESYSPATH, getter_Transfers(str)); + if (SUCCEEDED(hr)) { + aResultString.Assign(str.get()); + } + return hr; +} +} // namespace + +#define MOZ_ENSURE_HRESULT_OK(call_) \ + do { \ + HRESULT const _tmp_hr_ = (call_); \ + if (FAILED(_tmp_hr_)) return Err(_tmp_hr_); \ + } while (0) + +mozilla::Result<RefPtr<IFileDialog>, HRESULT> MakeFileDialog( + FileDialogType type) { + RefPtr<IFileDialog> dialog; + + CLSID const clsid = type == FileDialogType::Open ? CLSID_FileOpenDialog + : CLSID_FileSaveDialog; + HRESULT const hr = CoCreateInstance(clsid, nullptr, CLSCTX_INPROC_SERVER, + IID_IFileDialog, getter_AddRefs(dialog)); + MOZ_ENSURE_HRESULT_OK(hr); + + return std::move(dialog); +} + +HRESULT ApplyCommands(::IFileDialog* dialog, + nsTArray<Command> const& commands) { + Applicator applicator{.dialog = dialog}; + for (auto const& cmd : commands) { + HRESULT const hr = applicator.Visit(cmd); + if (FAILED(hr)) { + return hr; + } + } + return S_OK; +} + +mozilla::Result<Results, HRESULT> GetFileResults(::IFileDialog* dialog) { + FILEOPENDIALOGOPTIONS fos; + MOZ_ENSURE_HRESULT_OK(dialog->GetOptions(&fos)); + + using widget::WinUtils; + + // Extract which filter type the user selected + UINT index; + MOZ_ENSURE_HRESULT_OK(dialog->GetFileTypeIndex(&index)); + + // single selection + if ((fos & FOS_ALLOWMULTISELECT) == 0) { + RefPtr<IShellItem> item; + MOZ_ENSURE_HRESULT_OK(dialog->GetResult(getter_AddRefs(item))); + if (!item) { + return Err(E_FAIL); + } + + nsAutoString path; + MOZ_ENSURE_HRESULT_OK(GetShellItemPath(item, path)); + + return Results({path}, index); + } + + // multiple selection + RefPtr<IFileOpenDialog> openDlg; + dialog->QueryInterface(IID_IFileOpenDialog, getter_AddRefs(openDlg)); + if (!openDlg) { + MOZ_ASSERT(false, "a file-save dialog was given FOS_ALLOWMULTISELECT?"); + return Err(E_UNEXPECTED); + } + + RefPtr<IShellItemArray> items; + MOZ_ENSURE_HRESULT_OK(openDlg->GetResults(getter_AddRefs(items))); + if (!items) { + return Err(E_FAIL); + } + + nsTArray<nsString> paths; + + DWORD count = 0; + MOZ_ENSURE_HRESULT_OK(items->GetCount(&count)); + for (DWORD idx = 0; idx < count; idx++) { + RefPtr<IShellItem> item; + MOZ_ENSURE_HRESULT_OK(items->GetItemAt(idx, getter_AddRefs(item))); + + nsAutoString str; + MOZ_ENSURE_HRESULT_OK(GetShellItemPath(item, str)); + + paths.EmplaceBack(str); + } + + return Results(std::move(paths), std::move(index)); +} + +mozilla::Result<nsString, HRESULT> GetFolderResults(::IFileDialog* dialog) { + RefPtr<IShellItem> item; + MOZ_ENSURE_HRESULT_OK(dialog->GetResult(getter_AddRefs(item))); + if (!item) { + // shouldn't happen -- probably a precondition failure on our part, but + // might be due to misbehaving shell extensions? + MOZ_ASSERT(false, + "unexpected lack of item: was `Show`'s return value checked?"); + return Err(E_FAIL); + } + + // If the user chose a Win7 Library, resolve to the library's + // default save folder. + RefPtr<IShellLibrary> shellLib; + RefPtr<IShellItem> folderPath; + MOZ_ENSURE_HRESULT_OK( + CoCreateInstance(CLSID_ShellLibrary, nullptr, CLSCTX_INPROC_SERVER, + IID_IShellLibrary, getter_AddRefs(shellLib))); + + if (shellLib && SUCCEEDED(shellLib->LoadLibraryFromItem(item, STGM_READ)) && + SUCCEEDED(shellLib->GetDefaultSaveFolder(DSFT_DETECT, IID_IShellItem, + getter_AddRefs(folderPath)))) { + item.swap(folderPath); + } + + // get the folder's file system path + nsAutoString str; + MOZ_ENSURE_HRESULT_OK(GetShellItemPath(item, str)); + return str; +} + +#undef MOZ_ENSURE_HRESULT_OK + +namespace detail { +void LogProcessingError(LogModule* aModule, ipc::IProtocol* aCaller, + ipc::HasResultCodes::Result aCode, + const char* aReason) { + LogLevel const level = [&]() { + switch (aCode) { + case ipc::HasResultCodes::MsgProcessed: + // Normal operation. (We probably never actually get this code.) + return LogLevel::Verbose; + + case ipc::HasResultCodes::MsgDropped: + return LogLevel::Verbose; + + default: + return LogLevel::Error; + } + }(); + + // Processing errors are sometimes unhelpfully formatted. We can't fix that + // directly because the unhelpful formatting has made its way to telemetry + // (table `telemetry.socorro_crash`, column `ipc_channel_error`) and is being + // aggregated on. :( + nsCString reason(aReason); + if (reason.Last() == '\n') { + reason.Truncate(reason.Length() - 1); + } + + if (MOZ_LOG_TEST(aModule, level)) { + const char* const side = [&]() { + switch (aCaller->GetSide()) { + case ipc::ParentSide: + return "parent"; + case ipc::ChildSide: + return "child"; + case ipc::UnknownSide: + return "unknown side"; + default: + return "<illegal value>"; + } + }(); + + const char* const errorStr = [&]() { + switch (aCode) { + case ipc::HasResultCodes::MsgProcessed: + return "Processed"; + case ipc::HasResultCodes::MsgDropped: + return "Dropped"; + case ipc::HasResultCodes::MsgNotKnown: + return "NotKnown"; + case ipc::HasResultCodes::MsgNotAllowed: + return "NotAllowed"; + case ipc::HasResultCodes::MsgPayloadError: + return "PayloadError"; + case ipc::HasResultCodes::MsgProcessingError: + return "ProcessingError"; + case ipc::HasResultCodes::MsgRouteError: + return "RouteError"; + case ipc::HasResultCodes::MsgValueError: + return "ValueError"; + default: + return "<illegal error type>"; + } + }(); + + MOZ_LOG(aModule, level, + ("%s [%s]: IPC error (%s): %s", aCaller->GetProtocolName(), side, + errorStr, reason.get())); + } + + if (level == LogLevel::Error) { + // kill the child process... + if (aCaller->GetSide() == ipc::ParentSide) { + // ... which isn't us + ipc::UtilityProcessManager::GetSingleton()->CleanShutdown( + ipc::SandboxingKind::WINDOWS_FILE_DIALOG); + } else { + // ... which (presumably) is us + CrashReporter::AnnotateCrashReport( + CrashReporter::Annotation::ipc_channel_error, reason); + + MOZ_CRASH("IPC error"); + } + } +} + +// Given a (synchronous) Action returning a Result<T, HRESULT>, perform that +// action on a new single-purpose "File Dialog" thread, with COM initialized as +// STA. (The thread will be destroyed afterwards.) +// +// Returns a Promise which will resolve to T (if the action returns Ok) or +// reject with an HRESULT (if the action either returns Err or couldn't be +// performed). +template <typename Res, typename Action, size_t N> +RefPtr<Promise<Res>> SpawnFileDialogThread(const char (&where)[N], + Action action) { + RefPtr<nsIThread> thread; + { + nsresult rv = NS_NewNamedThread("File Dialog", getter_AddRefs(thread), + nullptr, {.isUiThread = true}); + if (NS_FAILED(rv)) { + return Promise<Res>::CreateAndReject((HRESULT)rv, where); + } + } + // `thread` is single-purpose, and should not perform any additional work + // after `action`. Shut it down after we've dispatched that. + auto close_thread_ = MakeScopeExit([&]() { + auto const res = thread->AsyncShutdown(); + static_assert( + std::is_same_v<uint32_t, std::underlying_type_t<decltype(res)>>); + if (NS_FAILED(res)) { + MOZ_LOG(sLogFileDialog, LogLevel::Warning, + ("thread->AsyncShutdown() failed: res=0x%08" PRIX32, + static_cast<uint32_t>(res))); + } + }); + + // our eventual return value + RefPtr promise = MakeRefPtr<typename Promise<Res>::Private>(where); + + // alias to reduce indentation depth + auto const dispatch = [&](auto closure) { + return thread->DispatchToQueue( + NS_NewRunnableFunction(where, std::move(closure)), + mozilla::EventQueuePriority::Normal); + }; + + dispatch([thread, promise, where, action = std::move(action)]() { + // Like essentially all COM UI components, the file dialog is STA: it must + // be associated with a specific thread to create its HWNDs and receive + // messages for them. If it's launched from a thread in the multithreaded + // apartment (including via implicit MTA), COM will proxy out to the + // process's main STA thread, and the file-dialog's modal loop will run + // there. + // + // This of course would completely negate any point in using a separate + // thread, since behind the scenes the dialog would still be running on the + // process's main thread. In particular, under that arrangement, file + // dialogs (and other nested modal loops, like those performed by + // `SpinEventLoopUntil`) will resolve in strictly LIFO order, effectively + // remaining suspended until all later modal loops resolve. + // + // To avoid this, we initialize COM as STA, so that it (rather than the main + // STA thread) is the file dialog's "home" thread and the IFileDialog's home + // apartment. + + mozilla::mscom::STARegion staRegion; + if (!staRegion) { + MOZ_LOG(sLogFileDialog, LogLevel::Error, + ("COM init failed on file dialog thread: hr = %08lx", + staRegion.GetHResult())); + + APTTYPE at; + APTTYPEQUALIFIER atq; + HRESULT const hr = ::CoGetApartmentType(&at, &atq); + MOZ_LOG(sLogFileDialog, LogLevel::Error, + (" current COM apartment state: hr = %08lX, APTTYPE = " + "%08X, APTTYPEQUALIFIER = %08X", + hr, at, atq)); + + // If this happens in the utility process, crash so we learn about it. + // (TODO: replace this with a telemetry ping.) + if (!XRE_IsParentProcess()) { + // Preserve relevant data on the stack for later analysis. + std::tuple volatile info{staRegion.GetHResult(), hr, at, atq}; + MOZ_CRASH("Could not initialize COM STA in utility process"); + } + + // If this happens in the parent process, don't crash; just fall back to a + // nested modal loop. This isn't ideal, but it will probably still work + // well enough for the common case, wherein no other modal loops are + // active. + // + // (TODO: replace this with a telemetry ping, too.) + } + + // Actually invoke the action and report the result. + Result<Res, HRESULT> val = action(); + if (val.isErr()) { + promise->Reject(val.unwrapErr(), where); + } else { + promise->Resolve(val.unwrap(), where); + } + }); + + return promise; +} + +// For F returning `Result<T, E>`, yields the type `T`. +template <typename F, typename... Args> +using inner_result_of = + typename std::remove_reference_t<decltype(std::declval<F>()( + std::declval<Args>()...))>::ok_type; + +template <typename ExtractorF, + typename RetT = inner_result_of<ExtractorF, IFileDialog*>> +auto SpawnPickerT(HWND parent, FileDialogType type, ExtractorF&& extractor, + nsTArray<Command> commands) -> RefPtr<Promise<Maybe<RetT>>> { + return detail::SpawnFileDialogThread<Maybe<RetT>>( + __PRETTY_FUNCTION__, + [=, commands = std::move(commands)]() -> Result<Maybe<RetT>, HRESULT> { + // On Win10, the picker doesn't support per-monitor DPI, so we create it + // with our context set temporarily to system-dpi-aware. + WinUtils::AutoSystemDpiAware dpiAwareness; + + RefPtr<IFileDialog> dialog; + MOZ_TRY_VAR(dialog, MakeFileDialog(type)); + + if (HRESULT const rv = ApplyCommands(dialog, commands); FAILED(rv)) { + return mozilla::Err(rv); + } + + if (HRESULT const rv = dialog->Show(parent); FAILED(rv)) { + if (rv == HRESULT_FROM_WIN32(ERROR_CANCELLED)) { + return Result<Maybe<RetT>, HRESULT>(Nothing()); + } + return mozilla::Err(rv); + } + + RetT res; + MOZ_TRY_VAR(res, extractor(dialog.get())); + + return Some(res); + }); +} + +} // namespace detail + +RefPtr<Promise<Maybe<Results>>> SpawnFilePicker(HWND parent, + FileDialogType type, + nsTArray<Command> commands) { + return detail::SpawnPickerT(parent, type, GetFileResults, + std::move(commands)); +} + +RefPtr<Promise<Maybe<nsString>>> SpawnFolderPicker(HWND parent, + nsTArray<Command> commands) { + return detail::SpawnPickerT(parent, FileDialogType::Open, GetFolderResults, + std::move(commands)); +} + +} // namespace mozilla::widget::filedialog diff --git a/widget/windows/filedialog/WinFileDialogCommands.h b/widget/windows/filedialog/WinFileDialogCommands.h new file mode 100644 index 0000000000..ca4561a8f2 --- /dev/null +++ b/widget/windows/filedialog/WinFileDialogCommands.h @@ -0,0 +1,74 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef widget_windows_filedialog_WinFileDialogCommands_h__ +#define widget_windows_filedialog_WinFileDialogCommands_h__ + +#include "ipc/EnumSerializer.h" +#include "mozilla/Logging.h" +#include "mozilla/MozPromise.h" +#include "mozilla/ipc/MessageLink.h" +#include "mozilla/widget/filedialog/WinFileDialogCommandsDefn.h" + +// Windows interface types, defined in <shobjidl.h> +struct IFileDialog; +struct IFileOpenDialog; + +namespace mozilla::widget::filedialog { + +extern LazyLogModule sLogFileDialog; + +enum class FileDialogType : uint8_t { Open, Save }; + +// Create a file-dialog of the relevant type. Requires MSCOM to be initialized. +mozilla::Result<RefPtr<IFileDialog>, HRESULT> MakeFileDialog(FileDialogType); + +// Apply the selected commands to the IFileDialog, in preparation for showing +// it. (The actual showing step is left to the caller.) +[[nodiscard]] HRESULT ApplyCommands(::IFileDialog*, + nsTArray<Command> const& commands); + +// Extract one or more results from the file-picker dialog. +// +// Requires that Show() has been called and has returned S_OK. +mozilla::Result<Results, HRESULT> GetFileResults(::IFileDialog*); + +// Extract the chosen folder from the folder-picker dialog. +// +// Requires that Show() has been called and has returned S_OK. +mozilla::Result<nsString, HRESULT> GetFolderResults(::IFileDialog*); + +namespace detail { +// Log the error. If it's a notable error, kill the child process. +void LogProcessingError(LogModule* aModule, ipc::IProtocol* aCaller, + ipc::HasResultCodes::Result aCode, const char* aReason); + +} // namespace detail + +template <typename R> +using Promise = MozPromise<R, HRESULT, true>; + +// Show a file-picker on another thread in the current process. +RefPtr<Promise<Maybe<Results>>> SpawnFilePicker(HWND parent, + FileDialogType type, + nsTArray<Command> commands); + +// Show a folder-picker on another thread in the current process. +RefPtr<Promise<Maybe<nsString>>> SpawnFolderPicker(HWND parent, + nsTArray<Command> commands); + +} // namespace mozilla::widget::filedialog + +namespace IPC { +template <> +struct ParamTraits<mozilla::widget::filedialog::FileDialogType> + : public ContiguousEnumSerializerInclusive< + mozilla::widget::filedialog::FileDialogType, + mozilla::widget::filedialog::FileDialogType::Open, + mozilla::widget::filedialog::FileDialogType::Save> {}; +} // namespace IPC + +#endif // widget_windows_filedialog_WinFileDialogCommands_h__ diff --git a/widget/windows/filedialog/WinFileDialogCommandsDefn.ipdlh b/widget/windows/filedialog/WinFileDialogCommandsDefn.ipdlh new file mode 100644 index 0000000000..dd85942f24 --- /dev/null +++ b/widget/windows/filedialog/WinFileDialogCommandsDefn.ipdlh @@ -0,0 +1,49 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set sw=2 ts=8 et ft=ipdl : */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +namespace mozilla { +namespace widget { +namespace filedialog { + +// Commands corresponding to the various functions in IFileDialog (or at least +// the ones we actually make use of). +// +// All commands' semantics are direct parallels of their equivalently-named +// functions on IFileDialog, with the only changes being those necessary to use +// IPDLable representation-datatypes. (Thus, e.g., `SetOptions` effectively +// takes a `FILEOPENDIALOGOPTIONS`, and `SetFileTypeIndex` is 1-based.) +struct SetOptions { uint32_t options; }; +struct SetTitle { nsString title; }; +struct SetOkButtonLabel { nsString label; }; +struct SetFolder { nsString path; }; +struct SetFileName { nsString filename; }; +struct SetDefaultExtension { nsString extension; }; +struct ComDlgFilterSpec { nsString name; nsString spec; }; +struct SetFileTypes { ComDlgFilterSpec[] filterList; }; +struct SetFileTypeIndex { uint32_t index; }; + +// Union of the above. +union Command { + SetOptions; + SetTitle; + SetOkButtonLabel; + SetFolder; + SetFileName; + SetDefaultExtension; + SetFileTypes; + SetFileTypeIndex; +}; + +// The results from opening a file dialog. (Note that folder selection only +// returns an nsString.) +struct Results { + nsString[] paths; + uint32_t selectedFileTypeIndex; +}; + +} // namespace filedialog +} // namespace widget +} // namespace mozilla diff --git a/widget/windows/filedialog/WinFileDialogParent.cpp b/widget/windows/filedialog/WinFileDialogParent.cpp new file mode 100644 index 0000000000..2c256a1506 --- /dev/null +++ b/widget/windows/filedialog/WinFileDialogParent.cpp @@ -0,0 +1,94 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "mozilla/widget/filedialog/WinFileDialogParent.h" + +#include "mozilla/Logging.h" +#include "mozilla/Result.h" +#include "mozilla/SpinEventLoopUntil.h" +#include "mozilla/ipc/UtilityProcessManager.h" +#include "nsISupports.h" + +namespace mozilla::widget::filedialog { + +// Count of currently-open file dialogs (not just open-file dialogs). +static size_t sOpenDialogActors = 0; + +WinFileDialogParent::WinFileDialogParent() { + MOZ_LOG(sLogFileDialog, LogLevel::Debug, + ("%s %p", __PRETTY_FUNCTION__, this)); +} + +WinFileDialogParent::~WinFileDialogParent() { + MOZ_LOG(sLogFileDialog, LogLevel::Debug, + ("%s %p", __PRETTY_FUNCTION__, this)); +} + +PWinFileDialogParent::nsresult WinFileDialogParent::BindToUtilityProcess( + mozilla::ipc::UtilityProcessParent* aUtilityParent) { + Endpoint<PWinFileDialogParent> parentEnd; + Endpoint<PWinFileDialogChild> childEnd; + nsresult rv = PWinFileDialog::CreateEndpoints(base::GetCurrentProcId(), + aUtilityParent->OtherPid(), + &parentEnd, &childEnd); + + if (NS_FAILED(rv)) { + MOZ_ASSERT(false, "Protocol endpoints failure"); + return NS_ERROR_FAILURE; + } + + if (!aUtilityParent->SendStartWinFileDialogService(std::move(childEnd))) { + MOZ_ASSERT(false, "SendStartWinFileDialogService failed"); + return NS_ERROR_FAILURE; + } + + if (!parentEnd.Bind(this)) { + MOZ_ASSERT(false, "parentEnd.Bind failed"); + return NS_ERROR_FAILURE; + } + + sOpenDialogActors++; + return NS_OK; +} + +void WinFileDialogParent::ProcessingError(Result aCode, const char* aReason) { + detail::LogProcessingError(sLogFileDialog, this, aCode, aReason); +} + +ProcessProxy::ProcessProxy(RefPtr<WFDP>&& obj) + : data(MakeRefPtr<Contents>(std::move(obj))) {} + +ProcessProxy::Contents::Contents(RefPtr<WFDP>&& obj) : ptr(std::move(obj)) {} + +ProcessProxy::Contents::~Contents() { + AssertIsOnMainThread(); + + // destroy the actor... + ptr->Close(); + + // ... and possibly the process + if (!--sOpenDialogActors) { + StopProcess(); + } +} + +void ProcessProxy::Contents::StopProcess() { + auto const upm = ipc::UtilityProcessManager::GetSingleton(); + if (!upm) { + // This is only possible when the UtilityProcessManager has shut down -- in + // which case the file-dialog process has also already been directed to shut + // down, and there's nothing we need to do here. + return; + } + + MOZ_LOG(sLogFileDialog, LogLevel::Debug, + ("%s: killing the WINDOWS_FILE_DIALOG process (no more live " + "actors)", + __PRETTY_FUNCTION__)); + upm->CleanShutdown(ipc::SandboxingKind::WINDOWS_FILE_DIALOG); +} + +} // namespace mozilla::widget::filedialog diff --git a/widget/windows/filedialog/WinFileDialogParent.h b/widget/windows/filedialog/WinFileDialogParent.h new file mode 100644 index 0000000000..a2c1197c55 --- /dev/null +++ b/widget/windows/filedialog/WinFileDialogParent.h @@ -0,0 +1,90 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef widget_windows_filedialog_WinFileDialogParent_h__ +#define widget_windows_filedialog_WinFileDialogParent_h__ + +#include "mozilla/Assertions.h" +#include "mozilla/Maybe.h" +#include "mozilla/MozPromise.h" +#include "mozilla/ProcInfo.h" +#include "mozilla/Result.h" +#include "mozilla/SpinEventLoopUntil.h" +#include "mozilla/dom/ChromeUtilsBinding.h" +#include "mozilla/ipc/UtilityProcessParent.h" +#include "mozilla/widget/filedialog/PWinFileDialogParent.h" +#include "nsISupports.h" +#include "nsStringFwd.h" + +namespace mozilla::widget::filedialog { + +class WinFileDialogParent : public PWinFileDialogParent { + public: + using UtilityActorName = ::mozilla::UtilityActorName; + NS_INLINE_DECL_THREADSAFE_REFCOUNTING(WinFileDialogParent, override); + + public: + WinFileDialogParent(); + nsresult BindToUtilityProcess( + mozilla::ipc::UtilityProcessParent* aUtilityParent); + + UtilityActorName GetActorName() { + return UtilityActorName::WindowsFileDialog; + } + + private: + ~WinFileDialogParent(); + + void ProcessingError(Result aCode, const char* aReason) override; +}; + +// Proxy for the WinFileDialog process and actor. +// +// The IPC subsystem holds a strong reference to all IPC actors, so releasing +// the last RefPtr for such an actor does not actually cause the actor to be +// destroyed. Similarly, the UtilityProcessManager owns the host process for an +// actor, and merely destroying all actors within that host process will not +// cause it to be reaped. +// +// This object, then, acts as a proxy for those objects' lifetimes: when the +// last reference to `Contents` is released, the necessary explicit cleanup of +// the actor (and, if possible, the host process) will be performed. +class ProcessProxy { + public: + using WFDP = WinFileDialogParent; + + explicit ProcessProxy(RefPtr<WFDP>&& obj); + ~ProcessProxy() = default; + + explicit operator bool() const { return data->ptr && data->ptr->CanSend(); } + bool operator!() const { return !bool(*this); } + + WFDP& operator*() const { return *data->ptr; } + WFDP* operator->() const { return data->ptr; } + WFDP* get() const { return data->ptr; } + + ProcessProxy(ProcessProxy const& that) = default; + ProcessProxy(ProcessProxy&&) = default; + + private: + struct Contents { + NS_INLINE_DECL_REFCOUNTING(Contents); + + public: + explicit Contents(RefPtr<WFDP>&& obj); + RefPtr<WFDP> const ptr; + + private: + ~Contents(); + void StopProcess(); + }; + // guaranteed nonnull + RefPtr<Contents> data; +}; + +} // namespace mozilla::widget::filedialog + +#endif // widget_windows_filedialog_WinFileDialogParent_h__ diff --git a/widget/windows/filedialog/moz.build b/widget/windows/filedialog/moz.build new file mode 100644 index 0000000000..d2732faf78 --- /dev/null +++ b/widget/windows/filedialog/moz.build @@ -0,0 +1,27 @@ +# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*- +# vim: set filetype=python: +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. + +IPDL_SOURCES += [ + "PWinFileDialog.ipdl", + "WinFileDialogCommandsDefn.ipdlh", +] + +UNIFIED_SOURCES += [ + "WinFileDialogChild.cpp", + "WinFileDialogCommands.cpp", + "WinFileDialogParent.cpp", +] + +EXPORTS.mozilla.widget.filedialog += [ + "WinFileDialogChild.h", + "WinFileDialogCommands.h", + "WinFileDialogParent.h", +] + +# needed for IPC header files +include("/ipc/chromium/chromium-config.mozbuild") + +FINAL_LIBRARY = "xul" |