/* -*- 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 "nsNativeAppSupportBase.h" #include "nsCOMPtr.h" #include "nsXPCOM.h" #include "nsISupportsPrimitives.h" #include "nsIObserverService.h" #include "nsIAppStartup.h" #include "nsServiceManagerUtils.h" #include "prlink.h" #include "nsXREDirProvider.h" #include "nsReadableUtils.h" #include "nsIFile.h" #include "nsDirectoryServiceDefs.h" #include "nsICommandLineRunner.h" #include "nsIWindowMediator.h" #include "nsPIDOMWindow.h" #include "nsIDocShell.h" #include "nsIBaseWindow.h" #include "nsIWidget.h" #include "nsIWritablePropertyBag2.h" #include "nsIPrefService.h" #include "mozilla/Services.h" #include <stdlib.h> #include <glib.h> #include <glib-object.h> #include <gtk/gtk.h> #ifdef MOZ_X11 #include <gdk/gdkx.h> #include <X11/ICE/ICElib.h> #include <X11/SM/SMlib.h> #include <fcntl.h> #include "nsThreadUtils.h" #include <pwd.h> #endif #ifdef MOZ_ENABLE_DBUS #include <dbus/dbus.h> #endif #define MIN_GTK_MAJOR_VERSION 2 #define MIN_GTK_MINOR_VERSION 10 #define UNSUPPORTED_GTK_MSG "We're sorry, this application requires a version of the GTK+ library that is not installed on your computer.\n\n\ You have GTK+ %d.%d.\nThis application requires GTK+ %d.%d or newer.\n\n\ Please upgrade your GTK+ library if you wish to use this application." #if MOZ_X11 #undef IceSetIOErrorHandler #undef IceAddConnectionWatch #undef IceConnectionNumber #undef IceProcessMessages #undef IceGetConnectionContext #undef SmcInteractDone #undef SmcSaveYourselfDone #undef SmcInteractRequest #undef SmcCloseConnection #undef SmcOpenConnection #undef SmcSetProperties typedef IceIOErrorHandler (*IceSetIOErrorHandlerFn) (IceIOErrorHandler); typedef int (*IceAddConnectionWatchFn) (IceWatchProc, IcePointer); typedef int (*IceConnectionNumberFn) (IceConn); typedef IceProcessMessagesStatus (*IceProcessMessagesFn) (IceConn, IceReplyWaitInfo*, Bool*); typedef IcePointer (*IceGetConnectionContextFn) (IceConn); typedef void (*SmcInteractDoneFn) (SmcConn, Bool); typedef void (*SmcSaveYourselfDoneFn) (SmcConn, Bool); typedef int (*SmcInteractRequestFn) (SmcConn, int, SmcInteractProc, SmPointer); typedef SmcCloseStatus (*SmcCloseConnectionFn) (SmcConn, int, char**); typedef SmcConn (*SmcOpenConnectionFn) (char*, SmPointer, int, int, unsigned long, SmcCallbacks*, const char*, char**, int, char*); typedef void (*SmcSetPropertiesFn) (SmcConn, int, SmProp**); static IceSetIOErrorHandlerFn IceSetIOErrorHandlerPtr; static IceAddConnectionWatchFn IceAddConnectionWatchPtr; static IceConnectionNumberFn IceConnectionNumberPtr; static IceProcessMessagesFn IceProcessMessagesPtr; static IceGetConnectionContextFn IceGetConnectionContextPtr; static SmcInteractDoneFn SmcInteractDonePtr; static SmcSaveYourselfDoneFn SmcSaveYourselfDonePtr; static SmcInteractRequestFn SmcInteractRequestPtr; static SmcCloseConnectionFn SmcCloseConnectionPtr; static SmcOpenConnectionFn SmcOpenConnectionPtr; static SmcSetPropertiesFn SmcSetPropertiesPtr; #define IceSetIOErrorHandler IceSetIOErrorHandlerPtr #define IceAddConnectionWatch IceAddConnectionWatchPtr #define IceConnectionNumber IceConnectionNumberPtr #define IceProcessMessages IceProcessMessagesPtr #define IceGetConnectionContext IceGetConnectionContextPtr #define SmcInteractDone SmcInteractDonePtr #define SmcSaveYourselfDone SmcSaveYourselfDonePtr #define SmcInteractRequest SmcInteractRequestPtr #define SmcCloseConnection SmcCloseConnectionPtr #define SmcOpenConnection SmcOpenConnectionPtr #define SmcSetProperties SmcSetPropertiesPtr enum ClientState { STATE_DISCONNECTED, STATE_REGISTERING, STATE_IDLE, STATE_INTERACTING, STATE_SHUTDOWN_CANCELLED }; static const char *gClientStateTable[] = { "DISCONNECTED", "REGISTERING", "IDLE", "INTERACTING", "SHUTDOWN_CANCELLED" }; static LazyLogModule sMozSMLog("MozSM"); #endif /* MOZ_X11 */ class nsNativeAppSupportUnix : public nsNativeAppSupportBase { public: #if MOZ_X11 nsNativeAppSupportUnix(): mSessionConnection(nullptr), mClientState(STATE_DISCONNECTED) {}; ~nsNativeAppSupportUnix() { // this goes out of scope after "web-workers-shutdown" async shutdown phase // so it's safe to disconnect here (i.e. the application won't lose data) DisconnectFromSM(); }; void DisconnectFromSM(); #endif NS_IMETHOD Start(bool* aRetVal); NS_IMETHOD Stop(bool *aResult); NS_IMETHOD Enable(); private: #if MOZ_X11 static void SaveYourselfCB(SmcConn smc_conn, SmPointer client_data, int save_style, Bool shutdown, int interact_style, Bool fast); static void DieCB(SmcConn smc_conn, SmPointer client_data); static void InteractCB(SmcConn smc_conn, SmPointer client_data); static void SaveCompleteCB(SmcConn smc_conn, SmPointer client_data) {}; static void ShutdownCancelledCB(SmcConn smc_conn, SmPointer client_data); void DoInteract(); void SetClientState(ClientState aState) { mClientState = aState; MOZ_LOG(sMozSMLog, LogLevel::Debug, ("New state = %s\n", gClientStateTable[aState])); } SmcConn mSessionConnection; ClientState mClientState; #endif }; #if MOZ_X11 static gboolean process_ice_messages(IceConn connection) { IceProcessMessagesStatus status; status = IceProcessMessages(connection, nullptr, nullptr); switch (status) { case IceProcessMessagesSuccess: return TRUE; case IceProcessMessagesIOError: { nsNativeAppSupportUnix *native = static_cast<nsNativeAppSupportUnix *>(IceGetConnectionContext(connection)); native->DisconnectFromSM(); } return FALSE; case IceProcessMessagesConnectionClosed: return FALSE; default: g_assert_not_reached (); } } static gboolean ice_iochannel_watch(GIOChannel *channel, GIOCondition condition, gpointer client_data) { return process_ice_messages(static_cast<IceConn>(client_data)); } static void ice_connection_watch(IceConn connection, IcePointer client_data, Bool opening, IcePointer *watch_data) { guint watch_id; if (opening) { GIOChannel *channel; int fd = IceConnectionNumber(connection); fcntl(fd, F_SETFD, fcntl(fd, F_GETFD, 0) | FD_CLOEXEC); channel = g_io_channel_unix_new(fd); watch_id = g_io_add_watch(channel, static_cast<GIOCondition>(G_IO_IN | G_IO_ERR), ice_iochannel_watch, connection); g_io_channel_unref(channel); *watch_data = GUINT_TO_POINTER(watch_id); } else { watch_id = GPOINTER_TO_UINT(*watch_data); g_source_remove(watch_id); } } static void ice_io_error_handler(IceConn connection) { // override the default handler which would exit the application; // do nothing and let ICELib handle the failure of the connection gracefully. } static void ice_init(void) { static bool initted = false; if (!initted) { IceSetIOErrorHandler(ice_io_error_handler); IceAddConnectionWatch(ice_connection_watch, nullptr); initted = true; } } void nsNativeAppSupportUnix::InteractCB(SmcConn smc_conn, SmPointer client_data) { nsNativeAppSupportUnix *self = static_cast<nsNativeAppSupportUnix *>(client_data); self->SetClientState(STATE_INTERACTING); // We do this asynchronously, as we spin the event loop recursively if // a dialog is displayed. If we do this synchronously, we don't finish // processing the current ICE event whilst the dialog is displayed, which // means we won't process any more. libsm hates us if we do the InteractDone // with a pending ShutdownCancelled, and we would certainly like to handle Die // whilst a dialog is displayed NS_DispatchToCurrentThread(NewRunnableMethod(self, &nsNativeAppSupportUnix::DoInteract)); } void nsNativeAppSupportUnix::DoInteract() { nsCOMPtr<nsIObserverService> obsServ = mozilla::services::GetObserverService(); if (!obsServ) { SmcInteractDone(mSessionConnection, False); SmcSaveYourselfDone(mSessionConnection, True); SetClientState(STATE_IDLE); return; } nsCOMPtr<nsISupportsPRBool> cancelQuit = do_CreateInstance(NS_SUPPORTS_PRBOOL_CONTRACTID); bool abortQuit = false; if (cancelQuit) { cancelQuit->SetData(false); obsServ->NotifyObservers(cancelQuit, "quit-application-requested", nullptr); cancelQuit->GetData(&abortQuit); } if (!abortQuit && mClientState == STATE_DISCONNECTED) { // The session manager disappeared, whilst we were interacting, so // quit now nsCOMPtr<nsIAppStartup> appService = do_GetService("@mozilla.org/toolkit/app-startup;1"); if (appService) { appService->Quit(nsIAppStartup::eForceQuit); } } else { if (mClientState != STATE_SHUTDOWN_CANCELLED) { // Only do this if the shutdown wasn't cancelled SmcInteractDone(mSessionConnection, !!abortQuit); SmcSaveYourselfDone(mSessionConnection, !abortQuit); } SetClientState(STATE_IDLE); } } void nsNativeAppSupportUnix::SaveYourselfCB(SmcConn smc_conn, SmPointer client_data, int save_style, Bool shutdown, int interact_style, Bool fast) { nsNativeAppSupportUnix *self = static_cast<nsNativeAppSupportUnix *>(client_data); // Expect a SaveYourselfCB if we're registering a new client. // All properties are already set in Start() so just reply with // SmcSaveYourselfDone if the callback matches the expected signature. // // Ancient versions (?) of xsm do not follow such an early SaveYourself with // SaveComplete. This is a problem if the application freezes interaction // while waiting for a response to SmcSaveYourselfDone. So never freeze // interaction when in STATE_REGISTERING. // // That aside, we could treat each combination of flags appropriately and not // special-case this. if (self->mClientState == STATE_REGISTERING) { self->SetClientState(STATE_IDLE); if (save_style == SmSaveLocal && interact_style == SmInteractStyleNone && !shutdown && !fast) { SmcSaveYourselfDone(self->mSessionConnection, True); return; } } if (self->mClientState == STATE_SHUTDOWN_CANCELLED) { // The last shutdown request was cancelled whilst we were interacting, // and we haven't finished interacting yet. Switch the state back again self->SetClientState(STATE_INTERACTING); } nsCOMPtr<nsIObserverService> obsServ = mozilla::services::GetObserverService(); if (!obsServ) { SmcSaveYourselfDone(smc_conn, True); return; } bool status = false; if (save_style != SmSaveGlobal) { nsCOMPtr<nsISupportsPRBool> didSaveSession = do_CreateInstance(NS_SUPPORTS_PRBOOL_CONTRACTID); if (!didSaveSession) { SmcSaveYourselfDone(smc_conn, True); return; } // Notify observers to save the session state didSaveSession->SetData(false); obsServ->NotifyObservers(didSaveSession, "session-save", nullptr); didSaveSession->GetData(&status); } // If the interact style permits us to, we are shutting down and we didn't // manage to (or weren't asked to) save the local state, then notify the user // in advance that we are doing to quit (assuming that we aren't already // doing so) if (!status && shutdown && interact_style != SmInteractStyleNone) { if (self->mClientState != STATE_INTERACTING) { SmcInteractRequest(smc_conn, SmDialogNormal, nsNativeAppSupportUnix::InteractCB, client_data); } } else { SmcSaveYourselfDone(smc_conn, True); } } void nsNativeAppSupportUnix::DieCB(SmcConn smc_conn, SmPointer client_data) { nsCOMPtr<nsIAppStartup> appService = do_GetService("@mozilla.org/toolkit/app-startup;1"); if (appService) { appService->Quit(nsIAppStartup::eForceQuit); } // Quit causes the shutdown to begin but the shutdown process is asynchronous // so we can't DisconnectFromSM() yet } void nsNativeAppSupportUnix::ShutdownCancelledCB(SmcConn smc_conn, SmPointer client_data) { nsNativeAppSupportUnix *self = static_cast<nsNativeAppSupportUnix *>(client_data); // Interacting is the only time when we wouldn't already have called // SmcSaveYourselfDone. Do that now, then set the state to make sure we // don't send it again after finishing interacting if (self->mClientState == STATE_INTERACTING) { SmcSaveYourselfDone(smc_conn, False); self->SetClientState(STATE_SHUTDOWN_CANCELLED); } } void nsNativeAppSupportUnix::DisconnectFromSM() { // the SM is free to exit any time after we disconnect, so callers must be // sure to have reached a sufficiently advanced phase of shutdown that there // is no risk of data loss: // e.g. all async writes are complete by the end of "profile-before-change" if (mSessionConnection) { SetClientState(STATE_DISCONNECTED); SmcCloseConnection(mSessionConnection, 0, nullptr); mSessionConnection = nullptr; gdk_x11_set_sm_client_id(nullptr); // follow gnome-client behaviour } } static void SetSMValue(SmPropValue& val, const nsCString& data) { val.value = static_cast<SmPointer>(const_cast<char*>(data.get())); val.length = data.Length(); } static void SetSMProperty(SmProp& prop, const char* name, const char* type, int numVals, SmPropValue vals[]) { prop.name = const_cast<char*>(name); prop.type = const_cast<char*>(type); prop.num_vals = numVals; prop.vals = vals; } #endif /* MOZ_X11 */ static void RemoveArg(char **argv) { do { *argv = *(argv + 1); ++argv; } while (*argv); --gArgc; } NS_IMETHODIMP nsNativeAppSupportUnix::Start(bool *aRetVal) { NS_ASSERTION(gAppData, "gAppData must not be null."); // The dbus library is used by both nsWifiScannerDBus and BluetoothDBusService, // from diffrent threads. This could lead to race conditions if the dbus is not // initialized before making any other library calls. #ifdef MOZ_ENABLE_DBUS dbus_threads_init_default(); #endif #if (MOZ_WIDGET_GTK == 2) if (gtk_major_version < MIN_GTK_MAJOR_VERSION || (gtk_major_version == MIN_GTK_MAJOR_VERSION && gtk_minor_version < MIN_GTK_MINOR_VERSION)) { GtkWidget* versionErrDialog = gtk_message_dialog_new(nullptr, GtkDialogFlags(GTK_DIALOG_MODAL | GTK_DIALOG_DESTROY_WITH_PARENT), GTK_MESSAGE_ERROR, GTK_BUTTONS_OK, UNSUPPORTED_GTK_MSG, gtk_major_version, gtk_minor_version, MIN_GTK_MAJOR_VERSION, MIN_GTK_MINOR_VERSION); gtk_dialog_run(GTK_DIALOG(versionErrDialog)); gtk_widget_destroy(versionErrDialog); MozExpectedExit(); exit(0); } #endif *aRetVal = true; #ifdef MOZ_X11 gboolean sm_disable = FALSE; if (!getenv("SESSION_MANAGER")) { sm_disable = TRUE; } nsAutoCString prev_client_id; char **curarg = gArgv + 1; while (*curarg) { char *arg = *curarg; if (arg[0] == '-' && arg[1] == '-') { arg += 2; if (!strcmp(arg, "sm-disable")) { RemoveArg(curarg); sm_disable = TRUE; continue; } else if (!strcmp(arg, "sm-client-id")) { RemoveArg(curarg); if (*curarg[0] != '-') { prev_client_id = *curarg; RemoveArg(curarg); } continue; } } ++curarg; } if (prev_client_id.IsEmpty()) { prev_client_id = getenv("DESKTOP_AUTOSTART_ID"); } // We don't want child processes to use the same ID unsetenv("DESKTOP_AUTOSTART_ID"); char *client_id = nullptr; if (!sm_disable) { PRLibrary *iceLib = PR_LoadLibrary("libICE.so.6"); if (!iceLib) { return NS_OK; } PRLibrary *smLib = PR_LoadLibrary("libSM.so.6"); if (!smLib) { PR_UnloadLibrary(iceLib); return NS_OK; } IceSetIOErrorHandler = (IceSetIOErrorHandlerFn)PR_FindFunctionSymbol(iceLib, "IceSetIOErrorHandler"); IceAddConnectionWatch = (IceAddConnectionWatchFn)PR_FindFunctionSymbol(iceLib, "IceAddConnectionWatch"); IceConnectionNumber = (IceConnectionNumberFn)PR_FindFunctionSymbol(iceLib, "IceConnectionNumber"); IceProcessMessages = (IceProcessMessagesFn)PR_FindFunctionSymbol(iceLib, "IceProcessMessages"); IceGetConnectionContext = (IceGetConnectionContextFn)PR_FindFunctionSymbol(iceLib, "IceGetConnectionContext"); if (!IceSetIOErrorHandler || !IceAddConnectionWatch || !IceConnectionNumber || !IceProcessMessages || !IceGetConnectionContext) { PR_UnloadLibrary(iceLib); PR_UnloadLibrary(smLib); return NS_OK; } SmcInteractDone = (SmcInteractDoneFn)PR_FindFunctionSymbol(smLib, "SmcInteractDone"); SmcSaveYourselfDone = (SmcSaveYourselfDoneFn)PR_FindFunctionSymbol(smLib, "SmcSaveYourselfDone"); SmcInteractRequest = (SmcInteractRequestFn)PR_FindFunctionSymbol(smLib, "SmcInteractRequest"); SmcCloseConnection = (SmcCloseConnectionFn)PR_FindFunctionSymbol(smLib, "SmcCloseConnection"); SmcOpenConnection = (SmcOpenConnectionFn)PR_FindFunctionSymbol(smLib, "SmcOpenConnection"); SmcSetProperties = (SmcSetPropertiesFn)PR_FindFunctionSymbol(smLib, "SmcSetProperties"); if (!SmcInteractDone || !SmcSaveYourselfDone || !SmcInteractRequest || !SmcCloseConnection || !SmcOpenConnection || !SmcSetProperties) { PR_UnloadLibrary(iceLib); PR_UnloadLibrary(smLib); return NS_OK; } ice_init(); // all callbacks are mandatory in libSM 1.0, so listen even if we don't care. unsigned long mask = SmcSaveYourselfProcMask | SmcDieProcMask | SmcSaveCompleteProcMask | SmcShutdownCancelledProcMask; SmcCallbacks callbacks; callbacks.save_yourself.callback = nsNativeAppSupportUnix::SaveYourselfCB; callbacks.save_yourself.client_data = static_cast<SmPointer>(this); callbacks.die.callback = nsNativeAppSupportUnix::DieCB; callbacks.die.client_data = static_cast<SmPointer>(this); callbacks.save_complete.callback = nsNativeAppSupportUnix::SaveCompleteCB; callbacks.save_complete.client_data = nullptr; callbacks.shutdown_cancelled.callback = nsNativeAppSupportUnix::ShutdownCancelledCB; callbacks.shutdown_cancelled.client_data = static_cast<SmPointer>(this); char errbuf[256]; mSessionConnection = SmcOpenConnection(nullptr, this, SmProtoMajor, SmProtoMinor, mask, &callbacks, prev_client_id.get(), &client_id, sizeof(errbuf), errbuf); } if (!mSessionConnection) { return NS_OK; } LogModule::Init(); // need to make sure initialized before SetClientState if (prev_client_id.IsEmpty() || (client_id && !prev_client_id.Equals(client_id))) { SetClientState(STATE_REGISTERING); } else { SetClientState(STATE_IDLE); } gdk_x11_set_sm_client_id(client_id); // Set SM Properties // SmCloneCommand, SmProgram, SmRestartCommand, SmUserID are required // properties so must be set, and must have a sensible fallback value. // Determine executable path to use for XSMP session restore // Is there a request to suppress default binary launcher? nsAutoCString path(getenv("MOZ_APP_LAUNCHER")); if (path.IsEmpty()) { NS_ASSERTION(gDirServiceProvider, "gDirServiceProvider is NULL! This shouldn't happen!"); nsCOMPtr<nsIFile> executablePath; nsresult rv; bool dummy; rv = gDirServiceProvider->GetFile(XRE_EXECUTABLE_FILE, &dummy, getter_AddRefs(executablePath)); if (NS_SUCCEEDED(rv)) { // Strip off the -bin suffix to get the shell script we should run; this is what Breakpad does nsAutoCString leafName; rv = executablePath->GetNativeLeafName(leafName); if (NS_SUCCEEDED(rv) && StringEndsWith(leafName, NS_LITERAL_CSTRING("-bin"))) { leafName.SetLength(leafName.Length() - strlen("-bin")); executablePath->SetNativeLeafName(leafName); } executablePath->GetNativePath(path); } } if (path.IsEmpty()) { // can't determine executable path. Best fallback is name from // application.ini but it might not resolve to the same executable at // launch time. path = gAppData->name; // will always be set ToLowerCase(path); MOZ_LOG(sMozSMLog, LogLevel::Warning, ("Could not determine executable path. Falling back to %s.", path.get())); } SmProp propRestart, propClone, propProgram, propUser, *props[4]; SmPropValue valsRestart[3], valsClone[1], valsProgram[1], valsUser[1]; int n = 0; NS_NAMED_LITERAL_CSTRING(kClientIDParam, "--sm-client-id"); SetSMValue(valsRestart[0], path); SetSMValue(valsRestart[1], kClientIDParam); SetSMValue(valsRestart[2], nsDependentCString(client_id)); SetSMProperty(propRestart, SmRestartCommand, SmLISTofARRAY8, 3, valsRestart); props[n++] = &propRestart; SetSMValue(valsClone[0], path); SetSMProperty(propClone, SmCloneCommand, SmLISTofARRAY8, 1, valsClone); props[n++] = &propClone; nsAutoCString appName(gAppData->name); // will always be set ToLowerCase(appName); SetSMValue(valsProgram[0], appName); SetSMProperty(propProgram, SmProgram, SmARRAY8, 1, valsProgram); props[n++] = &propProgram; nsAutoCString userName; // username that started the program struct passwd* pw = getpwuid(getuid()); if (pw && pw->pw_name) { userName = pw->pw_name; } else { userName = NS_LITERAL_CSTRING("nobody"); MOZ_LOG(sMozSMLog, LogLevel::Warning, ("Could not determine user-name. Falling back to %s.", userName.get())); } SetSMValue(valsUser[0], userName); SetSMProperty(propUser, SmUserID, SmARRAY8, 1, valsUser); props[n++] = &propUser; SmcSetProperties(mSessionConnection, n, props); g_free(client_id); #endif /* MOZ_X11 */ return NS_OK; } NS_IMETHODIMP nsNativeAppSupportUnix::Stop(bool *aResult) { NS_ENSURE_ARG(aResult); *aResult = true; return NS_OK; } NS_IMETHODIMP nsNativeAppSupportUnix::Enable() { return NS_OK; } nsresult NS_CreateNativeAppSupport(nsINativeAppSupport **aResult) { nsNativeAppSupportBase* native = new nsNativeAppSupportUnix(); if (!native) return NS_ERROR_OUT_OF_MEMORY; *aResult = native; NS_ADDREF(*aResult); return NS_OK; }