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!