Bug 469990: Allow command line arguments to be handed off from a new Firefox/Thunderbird process to an existing one when necessary. r=spohl
☠☠ backed out by a8f1f28ae9b0 ☠ ☠
authorYuri <mozilla@yuriydev.com>
Mon, 04 Mar 2019 09:58:47 -0500
changeset 520185 a4bdd668c63799c218c76873591ddc72598f1fc1
parent 520184 559e8b22fdc5812619eec44c0ed96f2631f172b0
child 520186 c54ee394e03c12095ec81ea539a8972894a0b741
push id10862
push userffxbld-merge
push dateMon, 11 Mar 2019 13:01:11 +0000
treeherdermozilla-beta@a2e7f5c935da [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersspohl
bugs469990
milestone67.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
@@ -49,16 +49,68 @@ nsresult GetNativeWindowPointerFromDOMWi
         }
       }
     }
   }
 
   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:
+      NSApplicationActivateIgnoringOtherApps];
+  }
+}
+
 class nsNativeAppSupportCocoa : public nsNativeAppSupportBase {
  public:
   nsNativeAppSupportCocoa() : mCanShowUI(false) {}
 
   NS_IMETHOD Start(bool *aRetVal) override;
   NS_IMETHOD ReOpen() override;
   NS_IMETHOD Enable() override;
 
@@ -84,16 +136,118 @@ 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;
+    }
+  }
+
+  BOOL mozillaRestarting = NO;
+  if ([[[[NSProcessInfo processInfo] environment] objectForKey:@"MOZ_APP_RESTART"]
+       isEqualToString:@"1"]) {
+    // Update process completed or restarting the app for another reason.
+    // Triggered by an old instance that just quit.
+    mozillaRestarting = YES;
+  }
+
+  // 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;
+      }
+    }
+  }
+
+
+  // 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"];
+  BOOL runningInstanceFound =
+    [[NSRunningApplication runningApplicationsWithBundleIdentifier:
+       [[NSBundle mainBundle] bundleIdentifier]] count] > 1;
+  if (!shallProceedLikeNoRemote && !mozillaRestarting && runningInstanceFound) {
+    // 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 (or launched as -no-remote)!
+    // Let's register a notification listener here,
+    // In case future instances would want to notify us about command line arguments
+    // passed to them. Note, that if mozilla process is restarting, we still need to
+    // register for notifications.
+    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() {
   NS_OBJC_BEGIN_TRY_ABORT_BLOCK_NSRESULT;