/* 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 "NativeMessagingPortal.h"

#include <gio/gunixfdlist.h>
#include <glib.h>

#include "mozilla/ClearOnShutdown.h"
#include "mozilla/GUniquePtr.h"
#include "mozilla/Logging.h"
#include "mozilla/WidgetUtilsGtk.h"
#include "mozilla/widget/AsyncDBus.h"
#include "mozilla/dom/Promise.h"
#include "nsAppShell.h"

#include "prlink.h"

static mozilla::LazyLogModule gNativeMessagingPortalLog(
    "NativeMessagingPortal");

#ifdef MOZ_LOGGING
#  define LOG_NMP(...) \
    MOZ_LOG(gNativeMessagingPortalLog, mozilla::LogLevel::Debug, (__VA_ARGS__))
#else
#  define LOG_NMP(args)
#endif

#define GET_FUNC(func, lib) \
  func##_fn = (decltype(func##_fn))PR_FindFunctionSymbol(lib, #func)

static gint _g_unix_fd_list_get(GUnixFDList* list, gint index_,
                                GError** error) {
  static PRLibrary* gioLib = nullptr;
  static bool gioInitialized = false;
  static gint (*g_unix_fd_list_get_fn)(GUnixFDList* list, gint index_,
                                       GError** error) = nullptr;

  if (!gioInitialized) {
    gioInitialized = true;
    gioLib = PR_LoadLibrary("libgio-2.0.so.0");
    if (!gioLib) {
      return -1;
    }
    GET_FUNC(g_unix_fd_list_get, gioLib);
  }

  if (!g_unix_fd_list_get_fn) {
    return -1;
  }

  return g_unix_fd_list_get_fn(list, index_, error);
}

namespace mozilla::extensions {

NS_IMPL_ISUPPORTS(NativeMessagingPortal, nsINativeMessagingPortal)

/* static */
already_AddRefed<NativeMessagingPortal> NativeMessagingPortal::GetSingleton() {
  static StaticRefPtr<NativeMessagingPortal> sInstance;

  if (MOZ_UNLIKELY(!sInstance)) {
    sInstance = new NativeMessagingPortal();
    ClearOnShutdown(&sInstance);
  }

  return do_AddRef(sInstance);
}

static void LogError(const char* aMethod, const GError& aError) {
  g_warning("%s error: %s", aMethod, aError.message);
}

static void RejectPromiseWithErrorMessage(dom::Promise& aPromise,
                                          const GError& aError) {
  aPromise.MaybeRejectWithOperationError(nsDependentCString(aError.message));
}

static nsresult GetPromise(JSContext* aCx, RefPtr<dom::Promise>& aPromise) {
  nsIGlobalObject* globalObject = xpc::CurrentNativeGlobal(aCx);
  if (NS_WARN_IF(!globalObject)) {
    return NS_ERROR_UNEXPECTED;
  }
  ErrorResult result;
  aPromise = dom::Promise::Create(globalObject, result);
  if (NS_WARN_IF(result.Failed())) {
    return result.StealNSResult();
  }
  return NS_OK;
}

struct CallbackData {
  explicit CallbackData(dom::Promise& aPromise,
                        const gchar* aSessionHandle = nullptr)
      : promise(&aPromise), sessionHandle(g_strdup(aSessionHandle)) {}
  RefPtr<dom::Promise> promise;
  GUniquePtr<gchar> sessionHandle;
  guint subscription_id = 0;
};

NativeMessagingPortal::NativeMessagingPortal() {
  LOG_NMP("NativeMessagingPortal::NativeMessagingPortal()");
  mCancellable = dont_AddRef(g_cancellable_new());
  nsAppShell::DBusConnectionCheck();
  g_dbus_proxy_new_for_bus(G_BUS_TYPE_SESSION, G_DBUS_PROXY_FLAGS_NONE, nullptr,
                           "org.freedesktop.portal.Desktop",
                           "/org/freedesktop/portal/desktop",
                           "org.freedesktop.portal.WebExtensions", mCancellable,
                           &NativeMessagingPortal::OnProxyReady, this);
}

NativeMessagingPortal::~NativeMessagingPortal() {
  LOG_NMP("NativeMessagingPortal::~NativeMessagingPortal()");

  g_cancellable_cancel(mCancellable);

  // Close all active sessions
  for (const auto& it : mSessions) {
    if (it.second != SessionState::Active) {
      continue;
    }
    GUniquePtr<GError> error;
    nsAppShell::DBusConnectionCheck();
    RefPtr<GDBusProxy> proxy = dont_AddRef(g_dbus_proxy_new_for_bus_sync(
        G_BUS_TYPE_SESSION, G_DBUS_PROXY_FLAGS_NONE, nullptr,
        "org.freedesktop.portal.Desktop", it.first.c_str(),
        "org.freedesktop.portal.Session", nullptr, getter_Transfers(error)));
    if (!proxy) {
      LOG_NMP("failed to get a D-Bus proxy: %s", error->message);
      LogError(__func__, *error);
      continue;
    }
    RefPtr<GVariant> res = dont_AddRef(
        g_dbus_proxy_call_sync(proxy, "Close", nullptr, G_DBUS_CALL_FLAGS_NONE,
                               -1, nullptr, getter_Transfers(error)));
    if (!res) {
      LOG_NMP("failed to close session: %s", error->message);
      LogError(__func__, *error);
    }
  }
}

NS_IMETHODIMP
NativeMessagingPortal::ShouldUse(bool* aResult) {
  *aResult = widget::ShouldUsePortal(widget::PortalKind::NativeMessaging);
  LOG_NMP("will %sbe used", *aResult ? "" : "not ");
  return NS_OK;
}

struct NativeMessagingPortal::DelayedCall {
  using DelayedMethodCall = void (NativeMessagingPortal::*)(dom::Promise&,
                                                            GVariant*);

  DelayedCall(DelayedMethodCall aCallback, dom::Promise& aPromise,
              GVariant* aArgs = nullptr)
      : callback(aCallback), promise(&aPromise), args(aArgs) {
    LOG_NMP("NativeMessagingPortal::DelayedCall::DelayedCall()");
  }
  ~DelayedCall() {
    LOG_NMP("NativeMessagingPortal::DelayedCall::~DelayedCall()");
  }

  DelayedMethodCall callback;
  RefPtr<dom::Promise> promise;
  RefPtr<GVariant> args;
};

/* static */
void NativeMessagingPortal::OnProxyReady(GObject* source, GAsyncResult* result,
                                         gpointer user_data) {
  NativeMessagingPortal* self = static_cast<NativeMessagingPortal*>(user_data);
  nsAppShell::DBusConnectionCheck();
  GUniquePtr<GError> error;
  self->mProxy = dont_AddRef(
      g_dbus_proxy_new_for_bus_finish(result, getter_Transfers(error)));
  if (self->mProxy) {
    LOG_NMP("D-Bus proxy ready for name %s, path %s, interface %s",
            g_dbus_proxy_get_name(self->mProxy),
            g_dbus_proxy_get_object_path(self->mProxy),
            g_dbus_proxy_get_interface_name(self->mProxy));
  } else {
    LOG_NMP("failed to get a D-Bus proxy: %s", error->message);
    LogError(__func__, *error);
  }
  self->mInitialized = true;
  while (!self->mPending.empty()) {
    auto pending = std::move(self->mPending.front());
    self->mPending.pop_front();
    (self->*pending->callback)(*pending->promise, pending->args.get());
  }
}

NS_IMETHODIMP
NativeMessagingPortal::GetAvailable(JSContext* aCx, dom::Promise** aPromise) {
  RefPtr<dom::Promise> promise;
  MOZ_TRY(GetPromise(aCx, promise));

  if (mInitialized) {
    MaybeDelayedIsAvailable(*promise, nullptr);
  } else {
    auto delayed = MakeUnique<DelayedCall>(
        &NativeMessagingPortal::MaybeDelayedIsAvailable, *promise);
    mPending.push_back(std::move(delayed));
  }

  promise.forget(aPromise);
  return NS_OK;
}

void NativeMessagingPortal::MaybeDelayedIsAvailable(dom::Promise& aPromise,
                                                    GVariant* aArgs) {
  MOZ_ASSERT(!aArgs);

  bool available = false;
  if (mProxy) {
    RefPtr<GVariant> version =
        dont_AddRef(g_dbus_proxy_get_cached_property(mProxy, "version"));
    if (version) {
      if (g_variant_get_uint32(version) >= 1) {
        available = true;
      }
    }
  }

  LOG_NMP("is %savailable", available ? "" : "not ");
  aPromise.MaybeResolve(available);
}

NS_IMETHODIMP
NativeMessagingPortal::CreateSession(const nsACString& aApplication,
                                     JSContext* aCx, dom::Promise** aPromise) {
  RefPtr<dom::Promise> promise;
  MOZ_TRY(GetPromise(aCx, promise));

  nsAutoCString token;
  widget::MakePortalRequestToken(PromiseFlatCString(aApplication), token);
  RefPtr<GVariant> args = dont_AddRef(g_variant_new_string(token.get()));

  if (mInitialized) {
    MaybeDelayedCreateSession(*promise, args);
  } else {
    auto delayed = MakeUnique<DelayedCall>(
        &NativeMessagingPortal::MaybeDelayedCreateSession, *promise, args);
    mPending.push_back(std::move(delayed));
  }

  promise.forget(aPromise);
  return NS_OK;
}

void NativeMessagingPortal::MaybeDelayedCreateSession(dom::Promise& aPromise,
                                                      GVariant* aArgs) {
  MOZ_ASSERT(g_variant_is_of_type(aArgs, G_VARIANT_TYPE_STRING));

  if (!mProxy) {
    return aPromise.MaybeRejectWithOperationError(
        "No D-Bus proxy for the native messaging portal");
  }

  LOG_NMP("creating session with handle suffix %s",
          g_variant_get_string(aArgs, nullptr));

  GVariantBuilder options;
  g_variant_builder_init(&options, G_VARIANT_TYPE_VARDICT);
  g_variant_builder_add(&options, "{sv}", "session_handle_token",
                        g_variant_ref_sink(aArgs));
  auto callbackData = MakeUnique<CallbackData>(aPromise);
  nsAppShell::DBusConnectionCheck();
  g_dbus_proxy_call(mProxy, "CreateSession", g_variant_new("(a{sv})", &options),
                    G_DBUS_CALL_FLAGS_NONE, -1, nullptr,
                    &NativeMessagingPortal::OnCreateSessionDone,
                    callbackData.release());
}

/* static */
void NativeMessagingPortal::OnCreateSessionDone(GObject* source,
                                                GAsyncResult* result,
                                                gpointer user_data) {
  GDBusProxy* proxy = G_DBUS_PROXY(source);
  UniquePtr<CallbackData> callbackData(static_cast<CallbackData*>(user_data));

  GUniquePtr<GError> error;
  RefPtr<GVariant> res = dont_AddRef(
      g_dbus_proxy_call_finish(proxy, result, getter_Transfers(error)));
  if (res) {
    RefPtr<GVariant> sessionHandle =
        dont_AddRef(g_variant_get_child_value(res, 0));
    gsize length;
    const char* value = g_variant_get_string(sessionHandle, &length);
    LOG_NMP("session created with handle %s", value);
    RefPtr<NativeMessagingPortal> portal = GetSingleton();
    portal->mSessions[value] = SessionState::Active;

    nsAppShell::DBusConnectionCheck();
    GDBusConnection* connection = g_dbus_proxy_get_connection(proxy);
    // The "Closed" signal is emitted e.g. when the user denies access to the
    // native application when the shell prompts them.
    auto subscription_id_ptr = MakeUnique<guint>(0);
    *subscription_id_ptr = g_dbus_connection_signal_subscribe(
        connection, "org.freedesktop.portal.Desktop",
        "org.freedesktop.portal.Session", "Closed", value, nullptr,
        G_DBUS_SIGNAL_FLAGS_NONE, &NativeMessagingPortal::OnSessionClosedSignal,
        subscription_id_ptr.get(), [](gpointer aUserData) {
          UniquePtr<guint> release(reinterpret_cast<guint*>(aUserData));
        });
    (void)subscription_id_ptr.release();  // Ownership transferred above.

    callbackData->promise->MaybeResolve(nsDependentCString(value, length));
  } else {
    LOG_NMP("failed to create session: %s", error->message);
    LogError(__func__, *error);
    RejectPromiseWithErrorMessage(*callbackData->promise, *error);
  }
}

NS_IMETHODIMP
NativeMessagingPortal::CloseSession(const nsACString& aHandle, JSContext* aCx,
                                    dom::Promise** aPromise) {
  const nsCString& sessionHandle = PromiseFlatCString(aHandle);

  if (!g_variant_is_object_path(sessionHandle.get())) {
    LOG_NMP("cannot close session %s, invalid handle", sessionHandle.get());
    return NS_ERROR_INVALID_ARG;
  }

  auto sessionIterator = mSessions.find(sessionHandle.get());
  if (sessionIterator == mSessions.end()) {
    LOG_NMP("cannot close session %s, unknown handle", sessionHandle.get());
    return NS_ERROR_INVALID_ARG;
  }

  if (sessionIterator->second != SessionState::Active) {
    LOG_NMP("cannot close session %s, not active", sessionHandle.get());
    return NS_ERROR_FAILURE;
  }

  RefPtr<dom::Promise> promise;
  MOZ_TRY(GetPromise(aCx, promise));

  sessionIterator->second = SessionState::Closing;
  LOG_NMP("closing session %s", sessionHandle.get());
  auto callbackData = MakeUnique<CallbackData>(*promise, sessionHandle.get());
  nsAppShell::DBusConnectionCheck();
  g_dbus_proxy_new_for_bus(
      G_BUS_TYPE_SESSION, G_DBUS_PROXY_FLAGS_NONE, nullptr,
      "org.freedesktop.portal.Desktop", sessionHandle.get(),
      "org.freedesktop.portal.Session", nullptr,
      &NativeMessagingPortal::OnCloseSessionProxyReady, callbackData.release());

  promise.forget(aPromise);
  return NS_OK;
}

/* static */
void NativeMessagingPortal::OnCloseSessionProxyReady(GObject* source,
                                                     GAsyncResult* result,
                                                     gpointer user_data) {
  UniquePtr<CallbackData> callbackData(static_cast<CallbackData*>(user_data));

  GUniquePtr<GError> error;
  RefPtr<GDBusProxy> proxy = dont_AddRef(
      g_dbus_proxy_new_for_bus_finish(result, getter_Transfers(error)));
  if (!proxy) {
    LOG_NMP("failed to close session: %s", error->message);
    LogError(__func__, *error);
    return RejectPromiseWithErrorMessage(*callbackData->promise, *error);
  }

  nsAppShell::DBusConnectionCheck();
  g_dbus_proxy_call(proxy, "Close", nullptr, G_DBUS_CALL_FLAGS_NONE, -1,
                    nullptr, &NativeMessagingPortal::OnCloseSessionDone,
                    callbackData.release());
}

/* static */
void NativeMessagingPortal::OnCloseSessionDone(GObject* source,
                                               GAsyncResult* result,
                                               gpointer user_data) {
  GDBusProxy* proxy = G_DBUS_PROXY(source);
  UniquePtr<CallbackData> callbackData(static_cast<CallbackData*>(user_data));

  RefPtr<NativeMessagingPortal> portal = GetSingleton();
  GUniquePtr<GError> error;
  nsAppShell::DBusConnectionCheck();
  RefPtr<GVariant> res = dont_AddRef(
      g_dbus_proxy_call_finish(proxy, result, getter_Transfers(error)));
  if (res) {
    LOG_NMP("session %s closed", callbackData->sessionHandle.get());
    portal->mSessions.erase(callbackData->sessionHandle.get());
    callbackData->promise->MaybeResolve(NS_OK);
  } else {
    LOG_NMP("failed to close session %s: %s", callbackData->sessionHandle.get(),
            error->message);
    LogError(__func__, *error);
    portal->mSessions[callbackData->sessionHandle.get()] = SessionState::Error;
    RejectPromiseWithErrorMessage(*callbackData->promise, *error);
  }
}

/* static */
void NativeMessagingPortal::OnSessionClosedSignal(
    GDBusConnection* bus, const gchar* sender_name, const gchar* object_path,
    const gchar* interface_name, const gchar* signal_name, GVariant* parameters,
    gpointer user_data) {
  nsAppShell::DBusConnectionCheck();
  guint subscription_id = *reinterpret_cast<guint*>(user_data);
  LOG_NMP("session %s was closed by the portal", object_path);
  g_dbus_connection_signal_unsubscribe(bus, subscription_id);
  RefPtr<NativeMessagingPortal> portal = GetSingleton();
  portal->mSessions.erase(object_path);
}

NS_IMETHODIMP
NativeMessagingPortal::GetManifest(const nsACString& aHandle,
                                   const nsACString& aName,
                                   const nsACString& aExtension, JSContext* aCx,
                                   dom::Promise** aPromise) {
  const nsCString& sessionHandle = PromiseFlatCString(aHandle);
  const nsCString& name = PromiseFlatCString(aName);
  const nsCString& extension = PromiseFlatCString(aExtension);

  if (!g_variant_is_object_path(sessionHandle.get())) {
    LOG_NMP("cannot find manifest for %s, invalid session handle %s",
            name.get(), sessionHandle.get());
    return NS_ERROR_INVALID_ARG;
  }

  auto sessionIterator = mSessions.find(sessionHandle.get());
  if (sessionIterator == mSessions.end()) {
    LOG_NMP("cannot find manifest for %s, unknown session handle %s",
            name.get(), sessionHandle.get());
    return NS_ERROR_INVALID_ARG;
  }

  if (sessionIterator->second != SessionState::Active) {
    LOG_NMP("cannot find manifest for %s, inactive session %s", name.get(),
            sessionHandle.get());
    return NS_ERROR_FAILURE;
  }

  if (!mProxy) {
    LOG_NMP("cannot find manifest for %s, missing D-Bus proxy", name.get());
    return NS_ERROR_FAILURE;
  }

  RefPtr<dom::Promise> promise;
  MOZ_TRY(GetPromise(aCx, promise));

  auto callbackData = MakeUnique<CallbackData>(*promise, sessionHandle.get());
  nsAppShell::DBusConnectionCheck();
  g_dbus_proxy_call(
      mProxy, "GetManifest",
      g_variant_new("(oss)", sessionHandle.get(), name.get(), extension.get()),
      G_DBUS_CALL_FLAGS_NONE, -1, nullptr,
      &NativeMessagingPortal::OnGetManifestDone, callbackData.release());

  promise.forget(aPromise);
  return NS_OK;
}

/* static */
void NativeMessagingPortal::OnGetManifestDone(GObject* source,
                                              GAsyncResult* result,
                                              gpointer user_data) {
  GDBusProxy* proxy = G_DBUS_PROXY(source);
  UniquePtr<CallbackData> callbackData(static_cast<CallbackData*>(user_data));

  GUniquePtr<GError> error;
  RefPtr<GVariant> jsonManifest = dont_AddRef(
      g_dbus_proxy_call_finish(proxy, result, getter_Transfers(error)));
  if (jsonManifest) {
    jsonManifest = dont_AddRef(g_variant_get_child_value(jsonManifest, 0));
    gsize length;
    const char* value = g_variant_get_string(jsonManifest, &length);
    LOG_NMP("manifest found in session %s: %s",
            callbackData->sessionHandle.get(), value);
    callbackData->promise->MaybeResolve(nsDependentCString(value, length));
  } else {
    LOG_NMP("failed to find a manifest in session %s: %s",
            callbackData->sessionHandle.get(), error->message);
    LogError(__func__, *error);
    RejectPromiseWithErrorMessage(*callbackData->promise, *error);
  }
}

NS_IMETHODIMP
NativeMessagingPortal::Start(const nsACString& aHandle, const nsACString& aName,
                             const nsACString& aExtension, JSContext* aCx,
                             dom::Promise** aPromise) {
  const nsCString& sessionHandle = PromiseFlatCString(aHandle);
  const nsCString& name = PromiseFlatCString(aName);
  const nsCString& extension = PromiseFlatCString(aExtension);

  if (!g_variant_is_object_path(sessionHandle.get())) {
    LOG_NMP("cannot start %s, invalid session handle %s", name.get(),
            sessionHandle.get());
    return NS_ERROR_INVALID_ARG;
  }

  auto sessionIterator = mSessions.find(sessionHandle.get());
  if (sessionIterator == mSessions.end()) {
    LOG_NMP("cannot start %s, unknown session handle %s", name.get(),
            sessionHandle.get());
    return NS_ERROR_INVALID_ARG;
  }

  if (sessionIterator->second != SessionState::Active) {
    LOG_NMP("cannot start %s, inactive session %s", name.get(),
            sessionHandle.get());
    return NS_ERROR_FAILURE;
  }

  if (!mProxy) {
    LOG_NMP("cannot start %s, missing D-Bus proxy", name.get());
    return NS_ERROR_FAILURE;
  }

  RefPtr<dom::Promise> promise;
  MOZ_TRY(GetPromise(aCx, promise));

  auto callbackData = MakeUnique<CallbackData>(*promise, sessionHandle.get());
  auto* releasedCallbackData = callbackData.release();

  LOG_NMP("starting %s, requested by %s in session %s", name.get(),
          extension.get(), sessionHandle.get());

  nsAutoCString token;
  widget::MakePortalRequestToken(extension, token);

  nsAutoCString requestPath;
  widget::GetPortalRequestPath(mProxy, token, requestPath);

  nsAppShell::DBusConnectionCheck();
  GDBusConnection* connection = g_dbus_proxy_get_connection(mProxy);
  releasedCallbackData->subscription_id = g_dbus_connection_signal_subscribe(
      connection, "org.freedesktop.portal.Desktop",
      "org.freedesktop.portal.Request", "Response", requestPath.get(), nullptr,
      G_DBUS_SIGNAL_FLAGS_NONE,
      &NativeMessagingPortal::OnStartRequestResponseSignal,
      releasedCallbackData, nullptr);

  auto callbackDataCopy =
      MakeUnique<CallbackData>(*promise, sessionHandle.get());
  GVariantBuilder options;
  g_variant_builder_init(&options, G_VARIANT_TYPE_VARDICT);
  g_variant_builder_add(&options, "{sv}", "handle_token",
                        g_variant_new_string(token.get()));
  g_dbus_proxy_call(mProxy, "Start",
                    g_variant_new("(ossa{sv})", sessionHandle.get(), name.get(),
                                  extension.get(), &options),
                    G_DBUS_CALL_FLAGS_NONE, -1, nullptr,
                    &NativeMessagingPortal::OnStartDone,
                    callbackDataCopy.release());

  promise.forget(aPromise);
  return NS_OK;
}

/* static */
void NativeMessagingPortal::OnStartDone(GObject* source, GAsyncResult* result,
                                        gpointer user_data) {
  nsAppShell::DBusConnectionCheck();
  GDBusProxy* proxy = G_DBUS_PROXY(source);
  UniquePtr<CallbackData> callbackData(static_cast<CallbackData*>(user_data));

  GUniquePtr<GError> error;
  RefPtr<GVariant> handle = dont_AddRef(
      g_dbus_proxy_call_finish(proxy, result, getter_Transfers(error)));
  if (handle) {
    handle = dont_AddRef(g_variant_get_child_value(handle, 0));
    LOG_NMP(
        "native application start requested in session %s, pending response "
        "for %s",
        callbackData->sessionHandle.get(),
        g_variant_get_string(handle, nullptr));
  } else {
    LOG_NMP("failed to start native application in session %s: %s",
            callbackData->sessionHandle.get(), error->message);
    LogError(__func__, *error);
    RejectPromiseWithErrorMessage(*callbackData->promise, *error);
  }
}

/* static */
void NativeMessagingPortal::OnStartRequestResponseSignal(
    GDBusConnection* bus, const gchar* sender_name, const gchar* object_path,
    const gchar* interface_name, const gchar* signal_name, GVariant* parameters,
    gpointer user_data) {
  nsAppShell::DBusConnectionCheck();
  UniquePtr<CallbackData> callbackData(static_cast<CallbackData*>(user_data));

  LOG_NMP("got response signal for %s in session %s", object_path,
          callbackData->sessionHandle.get());
  g_dbus_connection_signal_unsubscribe(bus, callbackData->subscription_id);

  RefPtr<GVariant> result =
      dont_AddRef(g_variant_get_child_value(parameters, 0));
  guint32 response = g_variant_get_uint32(result);
  // Possible values for response
  // (https://flatpak.github.io/xdg-desktop-portal/#gdbus-signal-org-freedesktop-portal-Request.Response):
  //   0: Success, the request is carried out
  //   1: The user cancelled the interaction
  //   2: The user interaction was ended in some other way
  if (response == 0) {
    LOG_NMP(
        "native application start successful in session %s, requesting file "
        "descriptors",
        callbackData->sessionHandle.get());
    RefPtr<NativeMessagingPortal> portal = GetSingleton();
    GVariantBuilder options;
    g_variant_builder_init(&options, G_VARIANT_TYPE_VARDICT);
    g_dbus_proxy_call_with_unix_fd_list(
        portal->mProxy.get(), "GetPipes",
        g_variant_new("(oa{sv})", callbackData->sessionHandle.get(), &options),
        G_DBUS_CALL_FLAGS_NONE, -1, nullptr, nullptr,
        &NativeMessagingPortal::OnGetPipesDone, callbackData.release());
  } else if (response == 1) {
    LOG_NMP("native application start canceled by user in session %s",
            callbackData->sessionHandle.get());
    callbackData->promise->MaybeRejectWithAbortError(
        "Native application start canceled by user");
  } else {
    LOG_NMP("native application start failed in session %s",
            callbackData->sessionHandle.get());
    callbackData->promise->MaybeRejectWithNotFoundError(
        "Native application start failed");
  }
}

static gint GetFD(const RefPtr<GVariant>& result, GUnixFDList* fds,
                  gint index) {
  RefPtr<GVariant> value =
      dont_AddRef(g_variant_get_child_value(result, index));
  GUniquePtr<GError> error;
  gint fd = _g_unix_fd_list_get(fds, g_variant_get_handle(value),
                                getter_Transfers(error));
  if (fd == -1) {
    LOG_NMP("failed to get file descriptor at index %d: %s", index,
            error->message);
    LogError("GetFD", *error);
  }
  return fd;
}

/* static */
void NativeMessagingPortal::OnGetPipesDone(GObject* source,
                                           GAsyncResult* result,
                                           gpointer user_data) {
  nsAppShell::DBusConnectionCheck();
  GDBusProxy* proxy = G_DBUS_PROXY(source);
  UniquePtr<CallbackData> callbackData(static_cast<CallbackData*>(user_data));
  auto promise = callbackData->promise;

  RefPtr<GUnixFDList> fds;
  GUniquePtr<GError> error;
  RefPtr<GVariant> pipes =
      dont_AddRef(g_dbus_proxy_call_with_unix_fd_list_finish(
          proxy, getter_AddRefs(fds), result, getter_Transfers(error)));

  if (!pipes) {
    LOG_NMP(
        "failed to get file descriptors for native application in session %s: "
        "%s",
        callbackData->sessionHandle.get(), error->message);
    LogError(__func__, *error);
    return RejectPromiseWithErrorMessage(*promise, *error);
  }

  gint32 _stdin = GetFD(pipes, fds, 0);
  gint32 _stdout = GetFD(pipes, fds, 1);
  gint32 _stderr = GetFD(pipes, fds, 2);
  LOG_NMP(
      "got file descriptors for native application in session %s: (%d, %d, %d)",
      callbackData->sessionHandle.get(), _stdin, _stdout, _stderr);

  if (_stdin == -1 || _stdout == -1 || _stderr == -1) {
    return promise->MaybeRejectWithOperationError("Invalid file descriptor");
  }

  dom::AutoJSAPI jsapi;
  if (NS_WARN_IF(!jsapi.Init(promise->GetGlobalObject()))) {
    return promise->MaybeRejectWithUnknownError(
        "Failed to initialize JS context");
  }
  JSContext* cx = jsapi.cx();

  JS::Rooted<JSObject*> jsPipes(cx, JS_NewPlainObject(cx));
  if (!jsPipes) {
    return promise->MaybeRejectWithOperationError(
        "Failed to create a JS object to hold the file descriptors");
  }

  auto setPipeProperty = [&](const char* name, int32_t value) {
    JS::Rooted<JS::Value> jsValue(cx, JS::Value::fromInt32(value));
    return JS_SetProperty(cx, jsPipes, name, jsValue);
  };
  if (!setPipeProperty("stdin", _stdin)) {
    return promise->MaybeRejectWithOperationError(
        "Failed to set the 'stdin' property on the JS object");
  }
  if (!setPipeProperty("stdout", _stdout)) {
    return promise->MaybeRejectWithOperationError(
        "Failed to set the 'stdout' property on the JS object");
  }
  if (!setPipeProperty("stderr", _stderr)) {
    return promise->MaybeRejectWithOperationError(
        "Failed to set the 'stderr' property on the JS object");
  }

  promise->MaybeResolve(jsPipes);
}

}  // namespace mozilla::extensions
