Bug 469990: Allow command line arguments to be handed off from a new Firefox/Thunderbird process to an existing one when necessary. r=spohl
authorYuri <mozilla@yuriydev.com>
Fri, 07 Dec 2018 09:33:40 -0500
changeset 508863 7bd4fa70b72870a1e8a6c7e39b3904202d7bcd40
parent 508862 6a4b98caa59b556990fbfc2c06fde08e851ef9fe
child 508864 b1e3a057032ea35502a6a9aecaea8ff6de04effe
push id1905
push userffxbld-merge
push dateMon, 21 Jan 2019 12:33:13 +0000
treeherdermozilla-release@c2fca1944d8c [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersspohl
bugs469990
milestone65.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Bug 469990: Allow command line arguments to be handed off from a new Firefox/Thunderbird process to an existing one when necessary. r=spohl
toolkit/xre/nsNativeAppSupportCocoa.mm
--- a/toolkit/xre/nsNativeAppSupportCocoa.mm
+++ b/toolkit/xre/nsNativeAppSupportCocoa.mm
@@ -3,16 +3,17 @@
  * 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 "nsString.h"
 
 #import <CoreServices/CoreServices.h>
 #import <Cocoa/Cocoa.h>
 
+#include "MacAutoreleasePool.h"
 #include "nsCOMPtr.h"
 #include "nsCocoaFeatures.h"
 #include "nsNativeAppSupportBase.h"
 
 #include "nsIAppShellService.h"
 #include "nsIAppStartup.h"
 #include "nsIBaseWindow.h"
 #include "nsCommandLine.h"
@@ -51,16 +52,67 @@ GetNativeWindowPointerFromDOMWindow(mozI
         }
       }
     }
   }
 
   return NS_OK;
 }
 
+// Essentially this notification handler implements the
+// "Mozilla remote" functionality on Mac, handing command line arguments (passed
+// to a newly launched process) to another copy of the current process that was
+// already running (which had registered the handler for this notification). All
+// other new copies just broadcast this notification and quit (unless -no-remote
+// was specified in either of these processes), making the original process handle
+// the arguments passed to this handler.
+void
+remoteClientNotificationCallback(CFNotificationCenterRef aCenter,
+                                 void* aObserver, CFStringRef aName,
+                                 const void* aObject,
+                                 CFDictionaryRef aUserInfo)
+{
+  // Autorelease pool to prevent memory leaks, in case there is no outer pool.
+  mozilla::MacAutoreleasePool pool;
+  NSDictionary* userInfoDict = (__bridge NSDictionary*)aUserInfo;
+  if (userInfoDict && [userInfoDict objectForKey:@"commandLineArgs"]) {
+    NSArray* args = [userInfoDict objectForKey:@"commandLineArgs"];
+    nsCOMPtr<nsICommandLineRunner> cmdLine(new nsCommandLine());
+
+    // Converting Objective-C array into a C array,
+    // which nsICommandLineRunner understands.
+    int argc = [args count];
+    const char** argv = new const char*[argc];
+    for (int i = 0; i < argc; i++) {
+      const char* arg = [[args objectAtIndex:i] UTF8String];
+      argv[i] = arg;
+    }
+
+    // We're not currently passing the working dir as third argument because it
+    // does not appear to be required.
+    nsresult rv = cmdLine->Init(argc, argv, nullptr,
+                                nsICommandLine::STATE_REMOTE_AUTO);
+
+    // Cleaning up C array.
+    delete[] argv;
+
+    if (NS_FAILED(rv)) {
+      NS_ERROR("Error initializing command line.");
+      return;
+    }
+
+    // Processing the command line, passed from a remote instance
+    // in the current instance.
+    cmdLine->Run();
+
+    // And bring the app's window to front.
+    [[NSRunningApplication currentApplication] activateWithOptions:0];
+  }
+}
+
 class nsNativeAppSupportCocoa : public nsNativeAppSupportBase
 {
 public:
   nsNativeAppSupportCocoa() :
     mCanShowUI(false) { }
 
   NS_IMETHOD Start(bool* aRetVal) override;
   NS_IMETHOD ReOpen() override;
@@ -90,16 +142,112 @@ NS_IMETHODIMP nsNativeAppSupportCocoa::S
   // localization.  So (for now at least) we just log an English message
   // to the console before quitting.
   if (major < 10 || minor < 6) {
     NSLog(@"Minimum OS version requirement not met!");
     return NS_OK;
   }
 
   *_retval = true;
+
+  // Here are the "special" CLI arguments that we can expect to be passed that
+  // should alter the default "hand args list to remote process and quit" algorithm:
+  // -headless : was already handled on macOS (allowing running multiple instances
+  // of the app), meaning this patch shouldn't break it.
+  // -no-remote : should always proceed, creating a second instance (which will
+  // fail on macOS, showing a MessageBox "Only one instance can be run at a time",
+  // unless a different profile dir path is specified).
+  // The rest of the arguments should be either passed on to
+  // the original running process (exiting the current process), or be processed by
+  // the current process (if -no-remote is specified).
+
+  mozilla::MacAutoreleasePool pool;
+  NSArray* arguments = [[NSProcessInfo processInfo] arguments];
+  BOOL shallProceedLikeNoRemote = NO;
+  for (NSString* arg in arguments) {
+    if ([arg isEqualToString:@"-no-remote"] || [arg isEqualToString:@"-headless"]) {
+      shallProceedLikeNoRemote = YES;
+      break;
+    }
+  }
+
+  // Apart from -no-remote, the user can specify an env variable
+  // MOZ_NO_REMOTE=1, which makes it behave the same way.
+  if (shallProceedLikeNoRemote == NO) {
+    NSDictionary* environmentVariables = [[NSProcessInfo processInfo] environment];
+    for (NSString* key in [environmentVariables allKeys]) {
+      if ([key isEqualToString:@"MOZ_NO_REMOTE"] &&
+          [environmentVariables[key] isEqualToString:@"1"]) {
+        shallProceedLikeNoRemote = YES;
+        break;
+      }
+    }
+  }
+
+  if (shallProceedLikeNoRemote) {
+    // Continue start up sequence. Do not register as a "first and only instance"
+    // listening for notifications, nor send notifications to another instance -
+    // essentially allowing launching multiple instances of the app.
+    *_retval = true;
+    return NS_OK;
+  }
+
+  // Now that we have handled no-remote-like arguments, at this point:
+  // 1) Either only the first instance of the process has been launched in any way
+  //    (.app double click, "open", "open -n", invoking executable in Terminal, etc.
+  // 2) Or the process has been launched with a "macos single instance" mechanism
+  //    override (using "open -n" OR directly by invoking the executable in Terminal
+  //    instead of clicking the .app bundle's icon, etc.).
+
+  // So, let's check if this is the first instance ever of the process for the
+  // current user.
+  NSString* notificationName = [[[NSBundle mainBundle] bundleIdentifier]
+                                  stringByAppendingString:
+                                  @".distributedNotification.commandLineArgs"];
+  if ([[NSRunningApplication runningApplicationsWithBundleIdentifier:
+        [[NSBundle mainBundle] bundleIdentifier]] count] > 1) {
+    // There is another instance of this app already running!
+    NSArray* arguments = [[NSProcessInfo processInfo] arguments];
+    CFDictionaryRef userInfoDict = (__bridge CFDictionaryRef)@{@"commandLineArgs":
+                                                                 arguments};
+
+    // This code is shared between Firefox, Thunderbird and other Mozilla products.
+    // So we need a notification name that is unique to the product, so we
+    // do not send a notification to Firefox from Thunderbird and so on. I am using
+    // bundle Id (assuming all Mozilla products come wrapped in .app bundles) -
+    // it should be unique
+    // (e.g., org.mozilla.firefox.distributedNotification.commandLineArgs for Firefox).
+    // We also need to make sure the notifications are "local" to the current user,
+    // so we do not pass it on to perhaps another running Thunderbird by another
+    // logged in user. Distributed notifications is the best candidate
+    // (while darwin notifications ignore the user context).
+    CFNotificationCenterPostNotification(CFNotificationCenterGetDistributedCenter(),
+                                         (__bridge CFStringRef)notificationName,
+                                         NULL,
+                                         userInfoDict,
+                                         true);
+
+    // Do not continue start up sequence for this process - just self-terminate,
+    // we already passed the arguments on to the original instance of the process.
+    *_retval = false;
+  } else {
+    // This is the first instance ever! Let's register a notification listener here,
+    // In case future instances would want to notify us about command line arguments
+    // passed to them
+    CFNotificationCenterAddObserver(CFNotificationCenterGetDistributedCenter(),
+                                    NULL,
+                                    remoteClientNotificationCallback,
+                                    (__bridge CFStringRef)notificationName,
+                                    NULL,
+                                    CFNotificationSuspensionBehaviorDeliverImmediately);
+
+    // Continue the start up sequence of this process.
+    *_retval = true;
+  }
+
   return NS_OK;
 
   NS_OBJC_END_TRY_ABORT_BLOCK_NSRESULT;
 }
 
 NS_IMETHODIMP
 nsNativeAppSupportCocoa::ReOpen()
 {