Page 1 of 2

Second instance of .exe fails

Posted: Wed Dec 20, 2023 12:17 pm
by Maksym
I'm on Windows 11, 64-bit. Delphi 2007. How to reproduce the issue: compile Simplebrowser2 demo in Delphi and run 2
instances of the compiled exe. The first one runs just fine, but the second one fails with an access violation every time on this line:

if GlobalCEFApp.StartMainProcess then

Version 119.4.7 is working just fine. So I had to go back to this version in my application. I tried versions 120.1.8 and 120.1.9 - the same issue on both. I didn't try any other versions between 119.4.7 and 120.1.8.

Re: Second instance of .exe fails

Posted: Wed Dec 20, 2023 12:23 pm
by salvadordf
Hi,

CEF has changed the way it initializes and now it checks if another app is running with the same RootCache setting.

This feature was added in CEF 120.1.8 :
https://www.briskbard.com/forum/viewtopic.php?p=9337#p9337

See the code comments for GlobalCEFApp.OnAlreadyRunningAppRelaunch, GlobalCEFApp.RootCache and GlobalCEFApp.Cache.

Re: Second instance of .exe fails

Posted: Wed Dec 20, 2023 1:09 pm
by Maksym
OK, thank you very much. So, basically, starting with version 120 there can be no multiple instances of the same app with the same RootCache folder. Am I right?

Re: Second instance of .exe fails

Posted: Wed Dec 20, 2023 3:03 pm
by salvadordf
Sharing a cache directory between several application instances was never supported by Chromium and it causes cache corruption.

If your application needs to open several browsers then run only one application instance and create new browsers in tabs, frames, child forms, etc.

Having multiple browsers in one application instance is the recommended way. See the TabbedBrowser2, ToolBoxBrowser and MDIBrowser demos for all the details.

Edit : More details in the official CEF forum : https://magpcss.org/ceforum/viewtopic.php?f=6&t=19665

Re: Second instance of .exe fails

Posted: Mon Jan 15, 2024 1:30 pm
by Maksym
OK, I've been trying to deal with issue for some time now. I'm on Delphi 2007, creating a 32-bit app. Here are my findings:

1. OnAlreadyRunningAppRelaunch is not called when I start the second instance. Here is the code:

Project.dpr:

----------

Code: Select all

CreateGlobalCEFApp;
  CanProceed := GlobalCefApp.StartMainProcess;
  if (CanProceed) then
  begin
    Application.Initialize;
    Application.MainFormOnTaskbar := True;
    Application.CreateForm(TMainForm, MainForm);
    Application.Run;
  end;

  DestroyGlobalCEFApp;
------------------

MainForm.pas:

------------------

Code: Select all

uses uCEFApplication;

procedure CreateGlobalCEFApp;
begin
  GlobalCEFApp := TCefApplication.Create;
  GlobalCEFApp.FrameworkDirPath := ExtractFilePath(ParamStr(0));
  GlobalCEFApp.ResourcesDirPath := ExtractFilePath(ParamStr(0));
  GlobalCEFApp.LocalesDirPath := ExtractFilePath(ParamStr(0)) + PathDelim + 'locales';
  GlobalCEFApp.RootCache := ExtractFilePath(ParamStr(0)) + 'Chrome Root';
  GlobalCEFApp.Cache := IncludeTrailingPathDelimiter(GlobalCEFApp.RootCache) + 'TheCache';

  GlobalCEFApp.OnWebKitInitialized := MainForm.GlobalCEFApp_OnWebKitInitialized;
  GlobalCEFApp.OnContextInitialized := MainForm.GlobalCEFApp_OnContextInitialized;
  GlobalCEFApp.OnAlreadyRunningAppRelaunch := MainForm.GlobalCEFApp_OnAlreadyRunningAppRelaunch;
end;

{ TMainForm }

procedure TMainForm.CEFInitializedMsg(var aMessage: TMessage);
begin
  Cursor := crDefault;
  Screen.Cursor := crDefault;

  OpenBrowserButton.Enabled := True;
end;

procedure TMainForm.GlobalCEFApp_OnAlreadyRunningAppRelaunch(
  const commandLine: ICefCommandLine; const current_directory: ustring;
  var aResult: boolean);
begin
  ShowMessage('GlobalCEFApp_OnAlreadyRunningAppRelaunch');
end;

procedure TMainForm.GlobalCEFApp_OnContextInitialized;
begin
  if (MainForm <> nil) and MainForm.HandleAllocated then
    PostMessage(MainForm.Handle, CEF_INITIALIZED, 0, 0);
end;

procedure TMainForm.GlobalCEFApp_OnWebKitInitialized;
begin
  ShowMessage('GlobalCEFApp_OnWebKitInitialized');
end;
----------------

Neither GlobalCEFApp_OnWebKitInitialized, nor GlobalCEFApp_OnAlreadyRunningAppRelaunch is getting called.

Another interesing finding. I would be just fine if I could create a mutext before even trying to initialize and start Chromium. But this code, if placed before the CreateGlobalCEFApp call, completely messes up Chromium - it simply doesn't work:

Code: Select all

  CreateMutex(nil, False, PAnsiChar('MutexChromium120'));
  if (GetLastError = ERROR_ALREADY_EXISTS) then
    Halt(0);
So, could you please give an example of how I should prevent the second instance from starting? All I need to do is pass the command line to the first instance and close the app.

Thank you in advance!

Re: Second instance of .exe fails

Posted: Mon Jan 15, 2024 4:01 pm
by salvadordf
I tested this event and GlobalCEFApp.OnAlreadyRunningAppRelaunch is triggered on the first application instance.

The second instance throws an exception inside the CEF library while executing GlobalCEFApp.InitializeLibrary and it should return false in GlobalCEFApp.StartMainProcess. This should cause that the second application to exit early without even initializing the main application form.

Your mutex code looks good but Chromium uses the same executable for all the subprocesses and the application should only try to create the mutex in the main browser process. Use this function and create the mutex when it returns FALSE :

Code: Select all

function IsCEFSubprocess : boolean;
var
  TempValue : ustring;
begin
  Result := GetCommandLineSwitchValue('type', TempValue) and (length(TempValue) > 0);
end;

Re: Second instance of .exe fails

Posted: Mon Jan 15, 2024 4:12 pm
by salvadordf
I just added that function to uCEFMiscFunctions.pas and it's available on GitHub.

Re: Second instance of .exe fails

Posted: Tue Jan 16, 2024 11:18 am
by Maksym
I tested this event and GlobalCEFApp.OnAlreadyRunningAppRelaunch is triggered on the first application instance.
Oh, yes. You are right. It does get triggered on the first instance. My mistake. Is there any way to get the command line of the second instance in this function? For example, instace one is launched like this:

C:\My App\MyApp.exe

and the second instance is launched like this:

C:\My App\MyApp.exe filename=d:\folder\file.txt

So, my task is to process "filename=d:\folder\file.txt" in the first instance and close the second instance quitly without any error messages. Right now it fails on both tasks. I do not get "filename=d:\folder\file.txt" in the commandLine parameter in the first instance's "OnAlreadyRunningAppRelaunch" function, plus I cannot "try except" the ceation and the start of the global CEF app. The exception seems to be happening on the other thread :(

Code: Select all

    try
      CreateGlobalCEFApp;
      CanProceed := GlobalCEFApp.StartMainProcess
    except
      on E: Exception do
      begin
        CanProceed := False; //this line never executes
      end;
    end;
The way I used to deal with it was creating a mutex and if it already exists - I send a Windows message with the command line of the second instance to the first instance, then closing the second instance without even getting to the creation of the GlobalCEFApp. What is the correct way of doing it with Chromium 120+?

Re: Second instance of .exe fails

Posted: Tue Jan 16, 2024 11:38 am
by Maksym
Correction. I can actually "try except" the creation and the start of the GlobalCEFApp. madExcept was preventing this. But I would still like to know how to accomplish my task "the correct way", without having to rely on the exception because I'd like to keep madExcept in the application.

Thank you very much for taking your time to answer my (probably stupid) questions!

Re: Second instance of .exe fails

Posted: Tue Jan 16, 2024 2:35 pm
by salvadordf
Both ways are correct and both work.

If you prefer to keep using the mutex you must call IsCEFSubprocess before trying to create the mutex like this :

Code: Select all

begin 
  if IsCEFSubprocess or CreateMyMutex then
    begin
      CreateGlobalCEFApp;
      
      if GlobalCEFApp.StarMainProcess then
        begin
          Application.Initialize;
          Application.CreateForm(TMainForm, MainForm);
          Application.Run;
        end;
        
      DestroyGlobalCEFApp;  
    end;
end;  
If you prefer to use the GlobalCEFApp.OnAlreadyRunningAppRelaunch event then you can read all the command line switches from the second instance in the first app instance.
You can use several methods with commandLine to get that information, for example:
  • commandLine.GetSwitchValue('customswitch')
  • commandLine.CommandLineString
Read the code comments for ICefCommandLine to know all the details.

You can test this event with MiniBrowser :

Code: Select all

procedure GlobalCEFApp_OnAlreadyRunningAppRelaunch(const commandLine: ICefCommandLine; const current_directory: ustring; var aResult: boolean);
begin
  OutputDebugString({$IFDEF DELPHI12_UP}PWideChar{$ELSE}PAnsiChar{$ENDIF}('GlobalCEFApp_OnAlreadyRunningAppRelaunch: ' + commandLine.GetSwitchValue('customswitch') + chr(0)));
  aResult := True;
end;

procedure CreateGlobalCEFApp;
begin
  GlobalCEFApp                            := TCefApplication.Create;
  GlobalCEFApp.cache                      := 'cache';
  GlobalCEFApp.EnablePrintPreview         := True;
  GlobalCEFApp.EnableGPU                  := True;
  GlobalCEFApp.LogFile                    := 'debug.log';
  GlobalCEFApp.LogSeverity                := LOGSEVERITY_INFO;
  GlobalCEFApp.UncaughtExceptionStackSize := 50;
  GlobalCEFApp.OnUncaughtException        := GlobalCEFApp_OnUncaughtException;
  GlobalCEFApp.OnAlreadyRunningAppRelaunch := GlobalCEFApp_OnAlreadyRunningAppRelaunch;
  //GlobalCEFApp.ChromeRuntime       := True;
end;
Run the first instance inside the Delphi IDE to see the "customswitch" value in the console.
Run the second instance with this command :

Code: Select all

MiniBrowser.exe --customswitch=1234