summaryrefslogtreecommitdiffstats
path: root/widget/windows/filedialog
diff options
context:
space:
mode:
Diffstat (limited to 'widget/windows/filedialog')
-rw-r--r--widget/windows/filedialog/PWinFileDialog.ipdl32
-rw-r--r--widget/windows/filedialog/WinFileDialogChild.cpp110
-rw-r--r--widget/windows/filedialog/WinFileDialogChild.h52
-rw-r--r--widget/windows/filedialog/WinFileDialogCommands.cpp460
-rw-r--r--widget/windows/filedialog/WinFileDialogCommands.h74
-rw-r--r--widget/windows/filedialog/WinFileDialogCommandsDefn.ipdlh49
-rw-r--r--widget/windows/filedialog/WinFileDialogParent.cpp94
-rw-r--r--widget/windows/filedialog/WinFileDialogParent.h90
-rw-r--r--widget/windows/filedialog/moz.build27
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"