Bug 1766125 - Allow setting profile creation directory for geckodriver, r=webdriver-reviewers,whimboo

This adds a `--profile-root` argument to geckodriver that sets the
directory in which we create temporary profiles.

This helps workaround an issue with sandboxed Firefox
instances (e.g. those running on snap) where the /tmp inside the
sandbox and outside the sandbox are not the same. Users can set this
to a directory that's visible both in and out of the sandbox so that
both geckodriver and Firefox can access the temporary profile.

Differential Revision: https://phabricator.services.mozilla.com/D144970
This commit is contained in:
James Graham 2022-05-18 13:26:08 +00:00
parent 4f3a3113bc
commit aa308fff96
10 changed files with 110 additions and 13 deletions

View File

@ -15,11 +15,16 @@ All notable changes to this program are documented in this file.
to access the system temporary directory. geckodriver uses the
temporary directory to store Firefox profiles created during the run.
This issue can be worked around by setting the `TMPDIR` environment
variable to a location that both Firefox and geckodriver have
read/write access to e.g.:
This issue can be worked around by using the `--profile-root`
command line option or setting the `TMPDIR` environment variable to
a location that both Firefox and geckodriver have read/write access
to e.g.:
% mkdir $HOME/tmp
% geckodriver --profile-root=~/tmp
or
% TMPDIR=$HOME/tmp geckodriver
Alternatively, geckodriver may be used with a Firefox install that

View File

@ -26,6 +26,7 @@ serde = "1.0"
serde_derive = "1.0"
serde_json = "1.0"
serde_yaml = "0.8"
tempfile = "3"
url = "2.0"
uuid = { version = "0.8", features = ["v4"] }
webdriver = { path = "../webdriver", version="0.45.0" }

View File

@ -190,6 +190,19 @@ Port to use for the WebDriver server. Defaults to 4444.
A helpful trick is that it is possible to bind to 0 to get the
system to atomically assign a free port.
## <code>&#x2D;&#x2D;profile-root <var>PROFILE_ROOT</var></code>
Path to the directory to use when creating temporary profiles. By
default this is the system temporary directory. Both geckodriver and
Firefox must have read-write access to this path.
This setting can be useful when Firefox is sandboxed from the host
filesystem such that it doesn't share the same system temporary
directory as geckodriver (e.g. when running Firefox inside a container
or packaged as a snap).
## <code>-v<var>[v]</var></code>
Increases the logging verbosity by to debug level when passing
@ -198,6 +211,7 @@ analogous to passing `--log debug` and `--log trace`, respectively.
[Marionette]: /testing/marionette/index.rst
## <code>&#x2D;&#x2D;websocket-port<var>PORT</var></code>
Port to use to connect to WebDriver BiDi. Defaults to 9222.

View File

@ -71,6 +71,7 @@ impl LocalBrowser {
options: FirefoxOptions,
marionette_port: u16,
jsdebugger: bool,
profile_root: Option<&Path>,
) -> WebDriverResult<LocalBrowser> {
let binary = options.binary.ok_or_else(|| {
WebDriverError::new(
@ -87,7 +88,7 @@ impl LocalBrowser {
let mut profile = match options.profile {
ProfileType::Named => None,
ProfileType::Path(x) => Some(x),
ProfileType::Temporary => Some(Profile::new()?),
ProfileType::Temporary => Some(Profile::new(profile_root)?),
};
let (profile_path, prefs_backup) = if let Some(ref mut profile) = profile {
@ -234,6 +235,7 @@ impl RemoteBrowser {
options: FirefoxOptions,
marionette_port: u16,
websocket_port: Option<u16>,
profile_root: Option<&Path>,
) -> WebDriverResult<RemoteBrowser> {
let android_options = options.android.unwrap();
@ -248,7 +250,7 @@ impl RemoteBrowser {
));
}
ProfileType::Path(x) => (x, true),
ProfileType::Temporary => (Profile::new()?, false),
ProfileType::Temporary => (Profile::new(profile_root)?, false),
};
set_prefs(
@ -398,7 +400,7 @@ mod tests {
// several regressions related to remote.log.level.
#[test]
fn test_remote_log_level() {
let mut profile = Profile::new().unwrap();
let mut profile = Profile::new(None).unwrap();
set_prefs(2828, &mut profile, false, vec![], false).ok();
let user_prefs = profile.user_prefs().unwrap();
@ -460,7 +462,7 @@ mod tests {
#[test]
fn test_pref_backup() {
let mut profile = Profile::new().unwrap();
let mut profile = Profile::new(None).unwrap();
// Create some prefs in the profile
let initial_prefs = profile.user_prefs().unwrap();

View File

@ -432,7 +432,10 @@ impl FirefoxOptions {
rv.env = FirefoxOptions::load_env(options)?;
rv.log = FirefoxOptions::load_log(options)?;
rv.prefs = FirefoxOptions::load_prefs(options)?;
if let Some(profile) = FirefoxOptions::load_profile(options)? {
if let Some(profile) = FirefoxOptions::load_profile(
settings.profile_root.as_ref().map(|x| x.as_path()),
options,
)? {
rv.profile = ProfileType::Path(profile);
}
}
@ -554,7 +557,10 @@ impl FirefoxOptions {
Ok(rv)
}
fn load_profile(options: &Capabilities) -> WebDriverResult<Option<Profile>> {
fn load_profile(
profile_root: Option<&Path>,
options: &Capabilities,
) -> WebDriverResult<Option<Profile>> {
if let Some(profile_json) = options.get("profile") {
let profile_base64 = profile_json.as_str().ok_or_else(|| {
WebDriverError::new(ErrorStatus::InvalidArgument, "Profile is not a string")
@ -562,7 +568,7 @@ impl FirefoxOptions {
let profile_zip = &*base64::decode(profile_base64)?;
// Create an emtpy profile directory
let profile = Profile::new()?;
let profile = Profile::new(profile_root)?;
unzip_buffer(
profile_zip,
profile

View File

@ -17,6 +17,7 @@ extern crate serde;
extern crate serde_derive;
extern crate serde_json;
extern crate serde_yaml;
extern crate tempfile;
extern crate url;
extern crate uuid;
extern crate webdriver;
@ -267,6 +268,20 @@ fn parse_args(app: &mut App) -> ProgramResult<Operation> {
let binary = args.value_of("binary").map(PathBuf::from);
let profile_root = args.value_of("profile_root").map(PathBuf::from);
// Try to create a temporary directory on startup to check that the directory exists and is writable
{
let tmp_dir = if let Some(ref tmp_root) = profile_root {
tempfile::tempdir_in(tmp_root)
} else {
tempfile::tempdir()
};
if tmp_dir.is_err() {
usage!("Unable to write to temporary directory; consider --profile-root with a writeable directory")
}
}
let marionette_host = args.value_of("marionette_host").unwrap();
let marionette_port = match args.value_of("marionette_port") {
Some(s) => match u16::from_str(s) {
@ -305,6 +320,7 @@ fn parse_args(app: &mut App) -> ProgramResult<Operation> {
let settings = MarionetteSettings {
binary,
profile_root,
connect_existing: args.is_present("connect_existing"),
host: marionette_host.into(),
port: marionette_port,
@ -473,6 +489,13 @@ fn make_app<'a>() -> App<'a> {
.long("version")
.help("Prints version and copying information"),
)
.arg(
Arg::new("profile_root")
.long("profile-root")
.takes_value(true)
.value_name("PROFILE_ROOT")
.help("Directory in which to create profiles. Defaults to the system temporary directory."),
)
.arg(
Arg::new("android_storage")
.long("android-storage")

View File

@ -81,6 +81,7 @@ struct MarionetteHandshake {
#[derive(Default)]
pub(crate) struct MarionetteSettings {
pub(crate) binary: Option<PathBuf>,
pub(crate) profile_root: Option<PathBuf>,
pub(crate) connect_existing: bool,
pub(crate) host: String,
pub(crate) port: Option<u16>,
@ -188,12 +189,14 @@ impl MarionetteHandler {
options,
marionette_port,
websocket_port,
self.settings.profile_root.as_ref().map(|x| x.as_path()),
)?)
} else if !self.settings.connect_existing {
Browser::Local(LocalBrowser::new(
options,
marionette_port,
self.settings.jsdebugger,
self.settings.profile_root.as_ref().map(|x| x.as_path()),
)?)
} else {
Browser::Existing(marionette_port)

View File

@ -26,8 +26,14 @@ impl PartialEq for Profile {
}
impl Profile {
pub fn new() -> IoResult<Profile> {
let dir = Builder::new().prefix("rust_mozprofile").tempdir()?;
pub fn new(temp_root: Option<&Path>) -> IoResult<Profile> {
let mut dir_builder = Builder::new();
dir_builder.prefix("rust_mozprofile");
let dir = if let Some(temp_root) = temp_root {
dir_builder.tempdir_in(temp_root)
} else {
dir_builder.tempdir()
}?;
let path = dir.path().to_path_buf();
let temp_dir = Some(dir);
Ok(Profile {

View File

@ -0,0 +1,36 @@
import copy
import os
import pytest
def test_profile_root(tmp_path, configuration, geckodriver):
profile_path = os.path.join(tmp_path, "geckodriver-test")
os.makedirs(profile_path)
config = copy.deepcopy(configuration)
# Ensure we don't set a profile in command line arguments
del config["capabilities"]["moz:firefoxOptions"]["args"]
extra_args = ["--profile-root", profile_path]
assert os.listdir(profile_path) == []
driver = geckodriver(config=config, extra_args=extra_args)
driver.new_session()
assert len(os.listdir(profile_path)) == 1
driver.delete_session()
assert os.listdir(profile_path) == []
def test_profile_root_missing(tmp_path, configuration, geckodriver):
profile_path = os.path.join(tmp_path, "missing-path")
config = copy.deepcopy(configuration)
# Ensure we don't set a profile in command line arguments
del config["capabilities"]["moz:firefoxOptions"]["args"]
extra_args = ["--profile-root", profile_path]
with pytest.raises(Exception):
geckodriver(config=config, extra_args=extra_args)

View File

@ -83,7 +83,8 @@ def geckodriver(configuration):
yield _geckodriver
driver.stop()
if driver is not None:
driver.stop()
class Browser: