Enumerating running Firefox browsers in Selenium

This is another article in our “Re-use selenium session” series. This article is specific to Firefox browser ran on a local system using Selenium

Consider the below python code

from selenium import webdriver

driver = webdriver.Firefox()

This opens up a firefox browser on your machine. We know that Selenium uses geckodriver for communicating with the firefox. So let’s check the process

$ ps aux | grep geckodriver
tarun.lalwani    11982   0.0  0.2  2565820  35736 s003  S+   12:18AM   0:00.27 geckodriver --port 65080

We have an interesting argument passed to the geckodriver which --port=65080. Let’s make a note of this. Now let’s get back to our python script and print the command executor url

print (driver.command_executor._url)
# prints 'http://127.0.0.1:65080'

As you can see this url has our previously noted down port. So by just looking at the geckodriver command line argument we can construct our command executor url. Now let’s use the command executor url on our terminal to check for the /sessions endpoint.

$ curl -sSL http://127.0.0.1:65080/sessions | jq
{
  "value": {
    "error": "unknown command",
    "message": "GET /sessions did not match a known command",
    "stacktrace": "stack backtrace:\n   0:        0x1088d2d31 - backtrace::backtrace::trace::h5db1675e0d2383fc\n   1:        0x1088d4224 - backtrace::capture::Backtrace::new::h8ca3ad60a3bf61a1\n   2:        0x108846dd4 - webdriver::error::WebDriverError::new::ha232d8b934114266\n   3:        0x1087e849f - _$LT$webdriver..httpapi..WebDriverHttpApi$LT$U$GT$$GT$::decode_request::he64889e38f370b23\n   4:        0x10882aea9 - _$LT$webdriver..server..HttpHandler$LT$U$GT$$u20$as$u20$hyper..server..Handler$GT$::handle::h5f9ac47fcf02b359\n   5:        0x10875e789 - _$LT$hyper..server..Worker$LT$H$GT$$GT$::keep_alive_loop::h5af4aefbc463d4e9\n   6:        0x10875f3d3 - _$LT$hyper..server..Worker$LT$H$GT$$GT$::handle_connection::h1b1946d602f5e047\n   7:        0x1087f5a07 - hyper::server::handle::_$u7b$$u7b$closure$u7d$$u7d$::h9f1845cdfece231e\n   8:        0x1087f5f67 - hyper::server::listener::spawn_with::_$u7b$$u7b$closure$u7d$$u7d$::h13066e1b4028d141\n   9:        0x10883a34e - _$LT$std..panic..AssertUnwindSafe$LT$F$GT$$u20$as$u20$core..ops..FnOnce$LT$$LP$$RP$$GT$$GT$::call_once::h278a443dead68f9e\n  10:        0x108772b6d - std::panicking::try::do_call::h36ab2d35fb06da16\n  11:        0x108e30fba - __rust_maybe_catch_panic\n  12:        0x108772530 - std::panicking::try::hc06ba68e7d4be053\n  13:        0x10876f258 - std::panic::catch_unwind::hf70512f0d4d31e94\n  14:        0x108771872 - std::thread::Builder::spawn::_$u7b$$u7b$closure$u7d$$u7d$::hcda9a568adb59aa5\n  15:        0x1087d04f6 - _$LT$F$u20$as$u20$alloc..boxed..FnBox$LT$A$GT$$GT$::call_box::h4b633025cd132136\n  16:        0x108e2f3e4 - std::sys::imp::thread::Thread::new::thread_start::hf2762ec0b24c4077\n  17:     0x7fff9420d93a - _pthread_body\n  18:     0x7fff9420d886 - _pthread_start"
  }
}

Unlike the chromedriver the /sessions endpoint is not supported by firefox. I am not sure what’s the reason behind the same, but this means we can’t find the driver’s session id the easy way.

Let’s see what info we can extract out of our geckodriver process.

$ lsof -a -c geckodriver -i4
COMMAND     PID          USER   FD   TYPE             DEVICE SIZE/OFF NODE NAME
geckodriv 11982 tarun.lalwani    3u  IPv4 0xb6a118be62ede50b      0t0  TCP localhost:65080 (LISTEN)
geckodriv 11982 tarun.lalwani    8u  IPv4 0xb6a118be84d016fb      0t0  TCP localhost:65131->localhost:65090 (ESTABLISHED)

As we can see, geckodriver has initiated a TCP connection on localhost port 65090. This means it is communicating with another process over port 65090. This can be confirmed using the below command

$ lsof -i:65090
COMMAND     PID          USER   FD   TYPE             DEVICE SIZE/OFF NODE NAME
geckodriv 11982 tarun.lalwani    8u  IPv4 0xb6a118be84d016fb      0t0  TCP localhost:65131->localhost:65090 (ESTABLISHED)
firefox-b 11983 tarun.lalwani   36u  IPv4 0xb6a118be630c5ff3      0t0  TCP localhost:65090 (LISTEN)
firefox-b 11983 tarun.lalwani   48u  IPv4 0xb6a118be84cf850b      0t0  TCP localhost:65090->localhost:65131 (ESTABLISHED)

The connection is to the firefox binary. So let’s just hit the url with a curl command

$ curl http://localhost:65090
50:{"applicationType":"gecko","marionetteProtocol":3}

This shows that it’s running the marionette server inside Firefox. You can finder more information about the same at Marionette - Mozilla | MDN

I started digging the Marionette code and the geckodriver source code to find pointers where we could extract the session id.

The code file documenting all endpoints of the geckodriver can be found here

The first command I found was /status.

$ curl -sS "http://localhost:65080/status" | jq
{
  "value": {
    "message": "Session already started",
    "ready": false
  }
}

No fruit here. As we can see in the source code there is no /sessions command supported. And rest commands required the sessionId to work.

Scanning rest of the code, I found interesting lead in session.rs

fn check_session(&self, msg: &WebDriverMessage<U>) -> WebDriverResult<()> {
    match msg.session_id {
        Some(ref msg_session_id) => {
            match self.session {
                Some(ref existing_session) => {
                    if existing_session.id != *msg_session_id {
                        Err(WebDriverError::new(
                            ErrorStatus::InvalidSessionId,
                            format!("Got unexpected session id {}",
                                    msg_session_id)))
                    } else {
                        Ok(())
                    }
                },
                None => Ok(())
            }
        },

This function is called for every session id we send and if the session ID is wrong, the exception contains the session. So next I executed a command with a dummy session id

curl -sS "http://localhost:65080/session/dummy/url" | jq
{
  "value": {
    "error": "invalid session id",
    "message": "Got unexpected session id dummy expected 7cec8257-f324-264a-a55d-4a7ae948a8f1",
    "stacktrace": "stack backtrace:\n   0:        0x1088d2d31 - backtrace::backtrace::trace::h5db1675e0d2383fc\n   1:        0x1088d4224 - backtrace::capture::Backtrace::new::h8ca3ad60a3bf61a1\n   2:        0x108846dd4 - webdriver::error::WebDriverError::new::ha232d8b934114266\n   3:        0x1087ea655 - _$LT$webdriver..server..Dispatcher$LT$T$C$$u20$U$GT$$GT$::check_session::he525a534a672d47f\n   4:        0x1087eae44 - _$LT$webdriver..server..Dispatcher$LT$T$C$$u20$U$GT$$GT$::run::h9fca7060c7339bba\n   5:        0x108847b18 - webdriver::server::start::_$u7b$$u7b$closure$u7d$$u7d$::h923bd0e628e644f0\n   6:        0x10883a29a - _$LT$std..panic..AssertUnwindSafe$LT$F$GT$$u20$as$u20$core..ops..FnOnce$LT$$LP$$RP$$GT$$GT$::call_once::h03d6be46931fe58b\n   7:        0x108772d59 - std::panicking::try::do_call::h403a44a5c5053ff1\n   8:        0x108e30fba - __rust_maybe_catch_panic\n   9:        0x108772243 - std::panicking::try::h329d006b4be2f87e\n  10:        0x10876f194 - std::panic::catch_unwind::hc2731cdb24566128\n  11:        0x10877155b - std::thread::Builder::spawn::_$u7b$$u7b$closure$u7d$$u7d$::h4473b1e218e8cfc5\n  12:        0x1087d0592 - _$LT$F$u20$as$u20$alloc..boxed..FnBox$LT$A$GT$$GT$::call_box::h8f8fd44e0cd4dd40\n  13:        0x108e2f3e4 - std::sys::imp::thread::Thread::new::thread_start::hf2762ec0b24c4077\n  14:     0x7fff9420d93a - _pthread_body\n  15:     0x7fff9420d886 - _pthread_start"
  }
}

Bingo we have the session id for our driver 7cec8257-f324-264a-a55d-4a7ae948a8f1.

We update our function to handle the first exception and create the driver with the intended session id

def create_driver_session_firefox(executor_url):
    _driver = create_driver_session("dummy", executor_url)

    try:
        _driver.current_url
    except WebDriverException as ex:
        session_id = ex.msg.replace("Got unexpected session id dummy expected ", "")
        if session_id:
            _driver = create_driver_session(session_id, executor_url)
            return _driver

    raise Exception("failed to find session id and create driver")

And now this works great!