JUCE icon indicating copy to clipboard operation
JUCE copied to clipboard

Windows vs Mac command line handling differs

Open kevin-- opened this issue 6 years ago • 4 comments

Building an app for Mac & Windows on JUCE 5.4.4. We subclass JUCEApplication and override initialise() and anotherInstanceStarted().

  • moreThanOnceInstanceAllowed is returning false

Let's say we launch the app myapp here are some args

Observed on Mac:

  1. initialise called with 1 string: here are some args
  2. anotherInstanceStarted 4 times, once for each arg: here, are, some, args

Observed on Windows:

  1. initialise called with 1 string: here are some args
  2. anotherInstanceStarted is not called at launch

Of course we can work around this, but would be nice to have a single code path to deal with, since we also want to take arguments from subsequent launches

kevin-- avatar Oct 14 '19 17:10 kevin--

This is due to the way that command line arguments are handled on Unix vs Windows. You should be able to use the JUCEApplicationBase::getCommandLineParameters() or JUCEApplicationBase::getCommandLineParameterArray() methods inside initialise() to get a platform-independent String or StringArray containing the args.

ed95 avatar Oct 15 '19 08:10 ed95

Hi @ed95, thanks for the info, but that is not exactly the issue I'm describing. It is rather whether or not anotherInstanceStarted is called on initial launch of the app.

kevin-- avatar Oct 16 '19 16:10 kevin--

Yeah this is problematic as it seems a new juce app started from a URL scheme cannot piggyback into the same app running, and will always start a new instance. This makes it impossible to implement deeplinking between the app and an externally launched URL.

Will this be addressed once and for all @ed95 ?

kunitoki avatar Feb 01 '23 00:02 kunitoki

In case someone else needs the solution, the Juce Team doesn't seem to care much.

diff --git a/juce/modules/juce_events/juce_events.cpp b/juce/modules/juce_events/juce_events.cpp
index f604f9033b804acf4d41cce70a640fbab9e0b158..3b4036042e2a0d8e59f7a7306b57466c9cc5db5b 100644
--- a/juce/modules/juce_events/juce_events.cpp
+++ b/juce/modules/juce_events/juce_events.cpp
@@ -51,6 +51,10 @@

 #elif JUCE_LINUX || JUCE_BSD
  #include <unistd.h>
+
+#elif JUCE_WINDOWS
+ #include <Psapi.h>
+
 #endif

 //==============================================================================
diff --git a/juce/modules/juce_events/native/juce_win32_Messaging.cpp b/juce/modules/juce_events/native/juce_win32_Messaging.cpp
index adfaf3b8c8e678cd952bdb882fc01336ede3a34e..f6695ee6a54fbbc92f9c192b9193561e0c8bd3e0 100644
--- a/juce/modules/juce_events/native/juce_win32_Messaging.cpp
+++ b/juce/modules/juce_events/native/juce_win32_Messaging.cpp
@@ -47,10 +47,15 @@ public:
     {
         messageWindow = std::make_unique<HiddenMessageWindow> (messageWindowName, (WNDPROC) messageWndProc);
         juce_messageWindowHandle = messageWindow->getHWND();
+
+        handleAnotherInstanceStarted();
     }

     ~InternalMessageQueue()
     {
+        if (uniqueMutex)
+            CloseHandle (uniqueMutex);
+
         juce_messageWindowHandle = nullptr;
         clearSingletonInstance();
     }
@@ -204,7 +209,29 @@ private:

     static void handleBroadcastMessage (const COPYDATASTRUCT* data)
     {
-        if (data != nullptr && data->dwData == broadcastMessageMagicNumber)
+        if (data == nullptr)
+            return;
+
+        if (data->dwData == anotherInstanceStartedMagicNumber)
+        {
+            String commandLine (CharPointer_UTF32 ((const CharPointer_UTF32::CharType*) data->lpData),
+                                data->cbData / sizeof (CharPointer_UTF32::CharType));
+
+            auto commandLineArgs = StringArray::fromTokens (commandLine, true);
+            if (! commandLineArgs.isEmpty())
+                commandLineArgs.remove (0);
+
+            struct AnotherInstanceStartedMessage  : public CallbackMessage
+            {
+                AnotherInstanceStartedMessage (String message) : message (std::move(message)) {}
+                void messageCallback() override { JUCEApplicationBase::getInstance()->anotherInstanceStarted (message); }
+
+                String message;
+            };
+
+            (new AnotherInstanceStartedMessage (commandLineArgs.joinIntoString (" ")))->post();
+        }
+        else if (data->dwData == broadcastMessageMagicNumber)
         {
             struct BroadcastMessage  : public CallbackMessage
             {
@@ -220,6 +247,104 @@ private:
         }
     }

+    static HWND getProcessMainWindow (DWORD processId)
+    {
+        HWND hwnd = NULL;
+
+        do
+        {
+            hwnd = FindWindowEx (NULL, hwnd, NULL, NULL);
+
+            DWORD dwPID = 0;
+            GetWindowThreadProcessId (hwnd, &dwPID);
+            if (dwPID == processId && GetWindow (hwnd, GW_OWNER) == (HWND) 0 && ! IsWindowVisible (hwnd))
+                return hwnd;
+        }
+        while (hwnd != NULL);
+    }
+
+    static HWND findFirstApplicationInstanceWindow (const String& applicationName)
+    {
+        constexpr int maxNumberOfProcesses = 8192;
+
+        DWORD numberOfProcessesNeeded;
+        HeapBlock<DWORD> processIds (maxNumberOfProcesses, true);
+
+        if (EnumProcesses (processIds, maxNumberOfProcesses * sizeof(DWORD), &numberOfProcessesNeeded))
+        {
+            const auto numberOfProcesses = static_cast<int> (numberOfProcessesNeeded / sizeof(DWORD));
+
+            WCHAR pathBuffer[MAX_PATH];
+
+            for (int processIndex = 0; processIndex < numberOfProcesses; ++processIndex)
+            {
+                const auto processId = processIds[processIndex];
+                if (processId == 0)
+                    continue;
+
+                HMODULE moduleHandle;
+                DWORD sizeNeeded;
+
+                HANDLE processHandle = OpenProcess (PROCESS_QUERY_INFORMATION | PROCESS_VM_READ, FALSE, processId);
+                if (processHandle == NULL)
+                    continue;
+
+                if (EnumProcessModules (processHandle, &moduleHandle, sizeof(moduleHandle), &sizeNeeded))
+                {
+                    juce::zeromem (pathBuffer, MAX_PATH * sizeof(WCHAR));
+
+                    auto moduleResult = GetModuleFileNameEx (processHandle, moduleHandle, pathBuffer, sizeof(pathBuffer) / sizeof(WCHAR));
+                    if (moduleResult == 0)
+                    {
+                        CloseHandle (processHandle);
+                        continue;
+                    }
+
+                    const auto processPath = File (String (pathBuffer));
+                    if (processPath.getFileNameWithoutExtension().compareIgnoreCase (applicationName) == 0)
+                    {
+                        HWND hwnd = getProcessMainWindow (processId);
+                        if (hwnd != NULL)
+                        {
+                            CloseHandle (processHandle);
+                            return hwnd;
+                        }
+                    }
+                }
+
+                CloseHandle (processHandle);
+            }
+        }
+
+        return NULL;
+    }
+
+    void handleAnotherInstanceStarted()
+    {
+        TCHAR szFileName[MAX_PATH] = { 0 };
+        GetModuleFileName (NULL, szFileName, MAX_PATH);
+
+        auto applicationName = File (String (szFileName)).getFileNameWithoutExtension();
+
+        String uniqueMutexName;
+        uniqueMutexName <<  "__unique_lock_" << applicationName;
+
+        uniqueMutex = CreateMutex (NULL, FALSE, uniqueMutexName.toWideCharPointer());
+        if (uniqueMutex == NULL || GetLastError() == ERROR_ALREADY_EXISTS)
+        {
+            const auto commandLineArguments = String (GetCommandLine());
+
+            COPYDATASTRUCT dataToSend = { 0 };
+            dataToSend.dwData = anotherInstanceStartedMagicNumber;
+            dataToSend.cbData = (DWORD) (((size_t) commandLineArguments.length() + 1) * sizeof (CharPointer_UTF32::CharType));
+            dataToSend.lpData = (void*) commandLineArguments.toUTF32().getAddress();
+
+            HWND firstApplicationInstanceWindow = findFirstApplicationInstanceWindow (applicationName);
+            if (firstApplicationInstanceWindow != NULL)
+                SendMessage (firstApplicationInstanceWindow, WM_COPYDATA, 0, (LPARAM) &dataToSend);
+        }
+    }
+
     void dispatchMessages()
     {
         ReferenceCountedArray<MessageManager::MessageBase> messagesToDispatch;
@@ -244,6 +369,7 @@ private:
     //==============================================================================
     static constexpr unsigned int customMessageID = WM_USER + 123;
     static constexpr unsigned int broadcastMessageMagicNumber = 0xc403;
+    static constexpr unsigned int anotherInstanceStartedMagicNumber = 0xf403;
     static const TCHAR messageWindowName[];

     std::unique_ptr<HiddenMessageWindow> messageWindow;
@@ -251,6 +377,8 @@ private:
     CriticalSection lock;
     ReferenceCountedArray<MessageManager::MessageBase> messageQueue;

+    HANDLE uniqueMutex;
+
     //==============================================================================
     JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (InternalMessageQueue)
 };

kunitoki avatar Feb 01 '23 11:02 kunitoki