Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Several issues and how I resolved them #3

Open
rfwoolf opened this issue Jul 7, 2024 · 0 comments
Open

Several issues and how I resolved them #3

rfwoolf opened this issue Jul 7, 2024 · 0 comments

Comments

@rfwoolf
Copy link

rfwoolf commented Jul 7, 2024

I'm not sure if this is the right place but it might be helpful to others if I share this

Firstly, thanks so much to Gabriel Trigo for this.
I have also used CEF4Delphi and I think one benefit of TSWebDriver4Delphi is how quickly you can get started.
There are some inherent complications in using a webdriver, for example if you want to use multiple automations simultaneously.
The problems I encountered were related to the webdriver constantly using the default installation and profile of Chrome.exe on the system. Thus I spent many, many hours getting a truly independent version of Chrome up and running.

Getting automations to run independently

I was tearing my hair out trying to get each automation to run independently.
Pro tip:
Navigate to chrome://version/ to see exactly which chrome.exe you are running and exactly which user profile you are running. This was a life-saver.
I downloaded Chrome Portable.
Next, ignore the file GoogleChromePortable\GoogleChromePortable.exe that it installs; the real file you want is GoogleChromePortable\App\Chrome-bin\chrome.exe
Make sure you download the latest chromedriver for that version and place it in the same folder as chrome.exe. Later we will tell the webdriver.exe to open that chrome.exe (and not the default chrome.exe)
Next, each server will need its own port number.
See the code changes below for how this gets implemented.
We will need to specifiy:

  1. The path to chrome.exe
  2. The path to webdriver.exe
  3. The path to the profiles directory, where it will load the 'default' profile
  4. A port for the server (you can use the default port of 9515 if you have only 1 automation at a time).

Evading bot detection

I was also unsuccessful in fully evading bot detection, despite implementing all the suggestions here
In my application, the bot detection was only a problem at the login page for the particular website I was visiting, so I had to code a solution where when the login page is detected, I close the automation, launch the browser without automation, log in, then close again, re-launch the automation, and continue!
For my logging in, I used TamperMonkey to automatically log in, and then I used my own browser-extension to close the browser after login.

So yes, one of the problems with chromedriver.exe is if you have a bot running an automation, and you try to launch chrome.exe as a human, it will fail bot detection. You have to close the automation.
Here is the code to launch chrome.exe but not as a bot, and wait for it to close. Reminder: if the bot is already running, this will just open another window for the bot! Make sure the bot is closed first

function LaunchUncontrolledBrowser(aURL : string = 'about:blank') : boolean;
var
  StartupInfo: TStartupInfo;
  ProcessInfo: TProcessInformation;
  ExecuteFile, Params, CommandLine, UserDataDir, ProfileDir: string;
  ExitCode: DWORD;
begin
  Result := False;
  ExecuteFile := 'C:\xxx\xxx\xx\xxx\GoogleChromePortable\App\Chrome-bin\chrome.exe';
  Params := aURL;
  UserDataDir := 'C:\xxx\xxx\xxx\xxx\GoogleChromePortable\Data'; //This is not the path to a specific profile, just the containing folder
  ProfileDir := 'default';  // Specify the profile directory if needed
  CommandLine := Format('"%s" --user-data-dir="%s" --profile-directory="%s" %s', [ExecuteFile, UserDataDir, ProfileDir, Params]);
  FillChar(StartupInfo, SizeOf(StartupInfo), 0);
  StartupInfo.cb := SizeOf(StartupInfo);
  if CreateProcess(nil, PChar(CommandLine), nil, nil, False, 0, nil, nil, StartupInfo, ProcessInfo) then
  begin
    // Wait for the process to finish
    WaitForSingleObject(ProcessInfo.hProcess, INFINITE);
    // Get the exit code
    GetExitCodeProcess(ProcessInfo.hProcess, ExitCode);
    // Close the process and thread handles
    CloseHandle(ProcessInfo.hProcess);
    CloseHandle(ProcessInfo.hThread);

    Result := True;
  end;
end;

My code to solve these problems

function StartWebSession: Boolean;
var
  WebDriverPath : string;
  port : integer;
  ChromePath : string;
  ChromeProfilesPath : string;
  HomePageURL : string;

HideDriverWindow : boolean;
begin
  result := false;

 //Initialise
//I put the webdriver in the same folder as the chrome.exe. Make sure it is the right version for the chrome you are using
  WebDriverPath := 'C:\xxx\xxx\xxx\xxx\GoogleChromePortable\App\Chrome-bin\webdriver.exe';
//The default port is 9515. If you are running multiple servers / automations, then each one should have its own port
  port := 9516
  ChromePath :='C:\xxx\xxx\xx\xxx\GoogleChromePortable\App\Chrome-bin\chrome.exe';
//The 'Data' directors can contain multiple profiles. Do not link to the profile itself, just the containing folder
  ChromeProfilesPath := 'C:\xxx\xxx\xxx\xxx\GoogleChromePortable\Data';
  HideDriverWindow := true;

  HomePageURL := 'www.example.com';
  //ReportMemoryLeaksOnShutdown := True;

  ///////////////////
  //Create the "Driver" (Server)
  ///////////////////
  FDriver := TTSWebDriver.New.Driver();

  //WebDriver
  WebDriverPath := StringReplace(WebDriverPath, '\', '/', [rfReplaceAll]);
  FDriver.Options.DriverPath(WebDriverPath);

  //ChromePath
  ChromePath := StringReplace(ChromePath, '\', '/', [rfReplaceAll]);
  FDriver.Options.BinaryPath(ChromePath);

  //////////////
  //Create the ChromeDriver
  //////////////
  FChromeDriver := FDriver.Browser().Chrome();
  FChromeDriver.AddArgument('disable-blink-features=AutomationControlled');
  FChromeDriver.AddArgument('excludeSwitches','enable-automation');
  FChromeDriver.AddArgument('useAutomationExtension','False');
  FChromeDriver.AddArgument('binary', ChromePath);
//If using Headlessmode, which makes the browser completely invisible to the user: 
  //FChromeDriver.AddArgument('--headless=new');
  //User data dir
  ChromeProfilesPath := StringReplace(ChromeProfilesPath, '\', '/', [rfReplaceAll]);
  FChromeDriver.AddArgument('user-data-dir', ChromeProfilesPath);

  //.AddArgument('window-size', '1000,800')

  //////////////
  //Start the server!
  //////////////
  //"Driver" (Server) Start
  //Here I have modified the 'Start' function to accept these other parameters (see code below)
  FDriver.Start(WebDriverPath, port, ChromePath, HideDriverWindow);

  //New Session
  FChromeDriver.NewSession();

  //Set webdriver to undefined
  FChromeDriver.ExecuteSyncScript('Object.defineProperty(navigator, ''webdriver'', {get: () => undefined})'); //{"script":"return document.title","parameters":{},"args":[]}

  //Network.setUserAgentOverride
  //============================
  //I was unable to execute 'Network.setUserAgentOverride' despite the right syntax due to the issue:
  //The Network.setUserAgentOverride command is part of the Chrome DevTools Protocol and must be executed within a context that supports this protocol.
  //Directly embedding this command in JavaScript running in the browser's console will not work, hence the "Network is not defined" error.
  //FChromeDriver.ExecuteSyncScript('Network.setUserAgentOverride', '{"userAgent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/108.0.0.0 Safari/537.36"}');

  //Navigate to...
  HomePageURL := StringReplace(HomePageURL, '\', '/', [rfReplaceAll]);
  FChromeDriver.NavigateTo(HomePageURL);

  result := true;
end;

Now modify the wrapper:
In unit TSWebDriver.Interfaces;
change
function Start(): ITSWebDriverBase; to:
function Start(executable_path : string; port : integer; binary_location : string; hidden: boolean = false): ITSWebDriverBase;

Now modify the unit unit TSWebDriver.Driver;:
change
function Start(): ITSWebDriverBase;
to
function Start(executable_path : string; port : integer ; binary_location : string; hidden: boolean = false): ITSWebDriverBase;

And make the function look like this:

function TTSWebDriverBase.Start(executable_path : string; port : integer; binary_location : string; hidden: boolean = false): ITSWebDriverBase;
var
  CommandLine: string;
begin
  FProccessName := ExtractFileName(FSWebDriverBaseOptions.DriverPath);

  if port = 0 then
    port := 9515;

  FPort := port;
  if not FileExists(FSWebDriverBaseOptions.DriverPath) then
    raise Exception.Create('driver file not exists.' + FSWebDriverBaseOptions.DriverPath);

  if Self.IsRunning() or (FProcessInfo.hProcess <> 0) then Exit;

  // StartupInfo
  FillChar(FStartupInfo, SizeOf(FStartupInfo), 0);
  if hidden then
    FStartupInfo.wShowWindow := SW_HIDE
  else
    FStartupInfo.wShowWindow := SW_NORMAL;
  FStartupInfo.dwFlags := STARTF_USESHOWWINDOW;

  // ProcessInfo
  FillChar(FProcessInfo, SizeOf(FProcessInfo), 0);

  // Construct the command line with the parameters
  CommandLine := Format('%s --port=%d --executable_path="' + executable_path + '" --binary_location="' + binary_location + '"', [FSWebDriverBaseOptions.DriverPath, port]);
  //CommandLine := Format('%s           --executable_path="' + executable_path + '" --binary_location="' + binary_location + '"', [FSWebDriverBaseOptions.DriverPath]);
  if CreateProcess(nil,                                             // lpApplicationName: PChar;          // pointer to name of executable module
                   PChar(CommandLine),                              // lpCommandLine: PChar;             // pointer to command line string
                   nil,                                             // lpProcessAttributes: PSecurityAttributes; // pointer to process security attributes
                   nil,                                             // lpThreadAttributes: PSecurityAttributes;  // pointer to thread security attributes
                   False,                                           // bInheritHandles: BOOL;            // handle inheritance flag
                   NORMAL_PRIORITY_CLASS,                           // dwCreationFlags: DWORD;           // creation flags
                   nil,                                             // lpEnvironment: Pointer;           // pointer to new environment block
                   nil,                                             // lpCurrentDirectory: PChar;        // pointer to current directory name
                   FStartupInfo,                                    // const lpStartupInfo: TStartupInfo;            // pointer to STARTUPINFO
                   FProcessInfo                                     // var lpProcessInformation: TProcessInformation // pointer to PROCESS_INFORMATION
  ) then
  begin
    // Process started successfully
  end
  else
  begin
    // Handle error
    RaiseLastOSError;
  end;
end;

Modify unit TSWebDriver.Consts;:
under interface, add this global variable:

var
  FPort : integer;
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

1 participant