123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537 |
- package Selenium::CanStartBinary;
- use strict;
- use warnings;
- # ABSTRACT: Teach a WebDriver how to start its own binary aka no JRE!
- use File::Spec;
- use Selenium::CanStartBinary::ProbePort
- qw/find_open_port_above find_open_port probe_port/;
- use Selenium::Firefox::Binary qw/setup_firefox_binary_env/;
- use Selenium::Waiter qw/wait_until/;
- use Moo::Role;
- use constant IS_WIN => $^O eq 'MSWin32';
- =for Pod::Coverage *EVERYTHING*
- =head1 DESCRIPTION
- This role takes care of the details for starting up a Webdriver
- instance. It does not do any downloading or installation of any sort -
- you're still responsible for obtaining and installing the necessary
- binaries into your C<$PATH> for this role to find. You may be
- interested in L<Selenium::Chrome>, L<Selenium::Firefox>, or
- L<Selenium::PhantomJS> if you're looking for classes that already
- consume this role.
- The role determines whether or not it should try to do its own magic
- based on whether the consuming class is instantiated with a
- C<remote_server_addr> and/or C<port>.
- # We'll start up the Chrome binary for you
- my $chrome_via_binary = Selenium::Chrome->new;
- # Look for a selenium server running on 4444.
- my $chrome_via_server = Selenium::Chrome->new( port => 4444 );
- If they're missing, we assume the user wants to use a webdriver
- directly and act accordingly. We handle finding the proper associated
- binary (or you can specify it with L</binary>), figuring out what
- arguments it wants, setting up any necessary environments, and
- starting up the binary.
- There's a number of TODOs left over - namely Windows support is
- severely lacking, and we're pretty naive when we attempt to locate the
- executables on our own.
- In the following documentation, C<required> refers to when you're
- consuming the role, not the C<required> when you're instantiating a
- class that has already consumed the role.
- =attr binary
- Required: Specify the path to the executable in question, or the name
- of the executable for us to find via L<File::Which/which>.
- =cut
- requires 'binary';
- =attr binary_port
- Required: Specify a default port that for the webdriver binary to try
- to bind to. If that port is unavailable, we'll probe above that port
- until we find a valid one.
- =cut
- requires 'binary_port';
- =attr _binary_args
- Required: Specify the arguments that the particular binary needs in
- order to start up correctly. In particular, you may need to tell the
- binary about the proper port when we start it up, or that it should
- use a particular prefix to match up with the behavior of the Remote
- Driver server.
- If your binary doesn't need any arguments, just have the default be an
- empty string.
- =cut
- requires '_binary_args';
- =attr port
- The role will attempt to determine the proper port for us. Consuming
- roles should set a default port in L</binary_port> at which we will
- begin searching for an open port.
- Note that if we cannot locate a suitable L</binary>, port will be set
- to 4444 so we can attempt to look for a Selenium server at
- C<127.0.0.1:4444>.
- =cut
- has '_real_binary' => (
- is => 'lazy',
- builder => sub {
- my ($self) = @_;
- if ( $self->_is_old_ff ) {
- return $self->firefox_binary;
- }
- else {
- return $self->binary;
- }
- }
- );
- has '_is_old_ff' => (
- is => 'lazy',
- builder => sub {
- my ($self) = @_;
- return $self->isa('Selenium::Firefox') && !$self->marionette_enabled;
- }
- );
- has '+port' => (
- is => 'lazy',
- builder => sub {
- my ($self) = @_;
- if ( $self->_real_binary ) {
- if ( $self->fixed_ports ) {
- return find_open_port( $self->binary_port );
- }
- else {
- return find_open_port_above( $self->binary_port );
- }
- }
- else {
- return 4444;
- }
- }
- );
- =attr fixed_ports
- Optional: By default, if binary_port and marionette_port are not free
- a higher free port is probed and acquired if possible, until a free one
- if found or a timeout is exceeded.
- my $driver1 = Selenium::Chrome->new;
- my $driver2 = Selenium::Chrome->new( port => 1234 );
- The default behavior can be overridden. In this case, only the default
- or given binary_port and marionette_port are probed, without probing
- higher ports. This ensures that either the default or given port will be
- assigned, or no port will be assigned at all.
- my $driver1 = Selenium::Chrome->new( fixed_ports => 1 );
- my $driver2 = Selenium::Chrome->new( port => 1234, fixed_ports => 1);
- =cut
- has 'fixed_ports' => (
- is => 'lazy',
- default => sub { 0 }
- );
- =attr custom_args
- Optional: If you want to pass additional options to the binary when it
- starts up, you can add that here. For example, if your binary accepts
- an argument on the command line like C<--log-path=/path/to/log>, and
- you'd like to specify that the binary uses that option, you could do:
- my $chrome = Selenium::Chrome->new(
- custom_args => '--log-path=/path/to/log'
- );
- To specify multiple arguments, just include them all in the string.
- =cut
- has custom_args => (
- is => 'lazy',
- predicate => 1,
- default => sub { '' }
- );
- has 'marionette_port' => (
- is => 'lazy',
- builder => sub {
- my ($self) = @_;
- if ( $self->_is_old_ff ) {
- return 0;
- }
- else {
- if ( $self->fixed_ports ) {
- return find_open_port( $self->marionette_binary_port );
- }
- else {
- return find_open_port_above( $self->marionette_binary_port );
- }
- }
- }
- );
- =attr startup_timeout
- Optional: you can modify how long we will wait for the binary to start
- up. By default, we will start the binary and check the intended
- destination port for 10 seconds before giving up. If the machine
- you're using to run your browsers is slower or smaller, you may need
- to increase this timeout.
- The following:
- my $f = Selenium::Firefox->new(
- startup_timeout => 60
- );
- will wait up to 60 seconds for the firefox binary to respond on the
- proper port. To use this constructor option, you should specify a time
- in seconds as an integer, and it will be passed to the arguments
- section of a L<Selenium::Waiter/wait_until> subroutine call.
- =cut
- has startup_timeout => (
- is => 'lazy',
- default => sub { 10 }
- );
- =attr binary_mode
- Mostly intended for internal use, its builder coordinates all the side
- effects of interacting with the binary: locating the executable,
- finding an open port, setting up the environment, shelling out to
- start the binary, and ensuring that the webdriver is listening on the
- correct port.
- If all of the above steps pass, it will return truthy after
- instantiation. If any of them fail, it should return falsy and the
- class should attempt normal L<Selenium::Remote::Driver> behavior.
- =cut
- has 'binary_mode' => (
- is => 'lazy',
- init_arg => undef,
- builder => 1,
- predicate => 1
- );
- has 'try_binary' => (
- is => 'lazy',
- default => sub { 0 },
- trigger => sub {
- my ($self) = @_;
- $self->binary_mode if $self->try_binary;
- }
- );
- =attr window_title
- Intended for internal use: this will build us a unique title for the
- background binary process of the Webdriver. Then, when we're cleaning
- up, we know what the window title is that we're going to C<taskkill>.
- =cut
- has 'window_title' => (
- is => 'lazy',
- init_arg => undef,
- builder => sub {
- my ($self) = @_;
- my ( undef, undef, $file ) =
- File::Spec->splitpath( $self->_real_binary );
- my $port = $self->port;
- return $file . ':' . $port;
- }
- );
- =attr command
- Intended for internal use: this read-only attribute is built by us,
- but it can be useful after instantiation to see exactly what command
- was run to start the webdriver server.
- my $f = Selenium::Firefox->new;
- say $f->_command;
- =cut
- has '_command' => (
- is => 'lazy',
- init_arg => undef,
- builder => sub {
- my ($self) = @_;
- return $self->_construct_command;
- }
- );
- =attr logfile
- Normally we log what occurs in the driver to /dev/null (or /nul on windows).
- Setting this will redirect it to the provided file.
- =cut
- has 'logfile' => (
- is => 'lazy',
- default => sub {
- return '/nul' if IS_WIN;
- return '/dev/null';
- }
- );
- sub BUILDARGS {
- # There's a bit of finagling to do to since we can't ensure the
- # attribute instantiation order. To decide whether we're going into
- # binary mode, we need the remote_server_addr and port. But, they're
- # both lazy and only instantiated immediately before S:R:D's
- # remote_conn attribute. Once remote_conn is set, we can't change it,
- # so we need the following order:
- #
- # parent: remote_server_addr, port
- # role: binary_mode (aka _build_binary_mode)
- # parent: remote_conn
- #
- # Since we can't force an order, we introduced try_binary which gets
- # decided during BUILDARGS to tip us off as to whether we should try
- # binary mode or not.
- my ( undef, %args ) = @_;
- if ( !exists $args{remote_server_addr} && !exists $args{port} ) {
- $args{try_binary} = 1;
- # Windows may throw a fit about invalid pointers if we try to
- # connect to localhost instead of 127.1
- $args{remote_server_addr} = '127.0.0.1';
- }
- else {
- $args{try_binary} = 0;
- $args{binary_mode} = 0;
- }
- return {%args};
- }
- sub _build_binary_mode {
- my ($self) = @_;
- # We don't know what to do without a binary driver to start up
- return unless $self->_real_binary;
- # Either the user asked for 4444, or we couldn't find an open port
- my $port = $self->port + 0;
- return if $port == 4444;
- if ( $self->fixed_ports && $port == 0 ) {
- die 'port '
- . $self->binary_port
- . ' is not free and have requested fixed ports';
- }
- $self->_handle_firefox_setup($port);
- system( $self->_command );
- my $success =
- wait_until { probe_port($port) } timeout => $self->startup_timeout;
- if ($success) {
- return 1;
- }
- else {
- die 'Unable to connect to the '
- . $self->_real_binary
- . ' binary on port '
- . $port;
- }
- }
- sub _handle_firefox_setup {
- my ( $self, $port ) = @_;
- # This is a no-op for other browsers
- return unless $self->isa('Selenium::Firefox');
- my $user_profile =
- $self->has_firefox_profile
- ? $self->firefox_profile
- : 0;
- my $profile =
- setup_firefox_binary_env( $port, $self->marionette_port, $user_profile );
- if ( $self->_is_old_ff ) {
- # For non-geckodriver/non-marionette, we want to get rid of
- # the profile so that we don't accidentally zip it and encode
- # it down the line while Firefox is trying to read from it.
- $self->clear_firefox_profile if $self->has_firefox_profile;
- }
- else {
- # For geckodriver/marionette, we keep the enhanced profile around because
- # we need to send it to geckodriver as a zipped b64-encoded
- # directory.
- $self->firefox_profile($profile);
- }
- }
- sub shutdown_binary {
- my ($self) = @_;
- return unless $self->auto_close();
- if ( defined $self->session_id ) {
- $self->quit();
- }
- if ( $self->has_binary_mode && $self->binary_mode ) {
- # Tell the binary itself to shutdown
- my $port = $self->port;
- my $ua = $self->ua;
- $ua->get( 'http://127.0.0.1:' . $port . '/wd/hub/shutdown' );
- # Close the orphaned command windows on windows
- $self->shutdown_windows_binary;
- $self->shutdown_unix_binary;
- }
- }
- sub shutdown_unix_binary {
- my ($self) = @_;
- if (!IS_WIN) {
- my $cmd = "lsof -t -i :".$self->port();
- my ( $pid ) = grep { $_ && $_ ne $$ } split( /\s+/, scalar `$cmd` );
- if ($pid) {
- print "Killing Driver PID $pid listening on port "
- . $self->port . "...\n";
- eval { kill 'KILL', $pid };
- warn
- "Could not kill driver process! you may have to clean up manually."
- if $@;
- }
- }
- }
- sub shutdown_windows_binary {
- my ($self) = @_;
- if (IS_WIN) {
- if ( $self->_is_old_ff ) {
- # FIXME: Blech, handle a race condition that kills the
- # driver before it's finished cleaning up its sessions. In
- # particular, when the perl process ends, it wants to
- # clean up the temp directory it created for the Firefox
- # profile. But, if the Firefox process is still running,
- # it will have a lock on the temp profile directory, and
- # perl will get upset. This "solution" is _very_ bad.
- sleep(2);
- # Firefox doesn't have a Driver/Session architecture - the
- # only thing running is Firefox itself, so there's no
- # other task to kill.
- return;
- }
- system( 'taskkill /FI "WINDOWTITLE eq '
- . $self->window_title
- . '" > nul 2>&1' );
- }
- }
- sub DEMOLISH {
- my ( $self, $in_gd ) = @_;
- # if we're in global destruction, all bets are off.
- return if $in_gd;
- $self->shutdown_binary;
- }
- sub _construct_command {
- my ($self) = @_;
- my $executable = $self->_real_binary;
- # Executable path names may have spaces
- $executable = '"' . $executable . '"';
- # The different binaries take different arguments for proper setup
- $executable .= $self->_binary_args;
- if ( $self->has_custom_args ) {
- $executable .= ' ' . $self->custom_args;
- }
- # Handle Windows vs Unix discrepancies for invoking shell commands
- my ( $prefix, $suffix ) = ( $self->_cmd_prefix, $self->_cmd_suffix );
- return join( ' ', ( $prefix, $executable, $suffix ) );
- }
- sub _cmd_prefix {
- my ($self) = @_;
- my $prefix = '';
- if (IS_WIN) {
- $prefix = 'start "' . $self->window_title . '"';
- if ( $self->_is_old_ff ) {
- # For older versions of Firefox that run without
- # marionette, the command we're running actually starts up
- # the browser itself, so we don't want to minimize it.
- return $prefix;
- }
- else {
- # If we're firefox with marionette, or any other browser,
- # the command we're running is the driver, and we don't
- # need want the command window in the foreground.
- return $prefix . ' /MIN ';
- }
- }
- return $prefix;
- }
- sub _cmd_suffix {
- my ($self) = @_;
- return " > " . $self->logfile . " 2>&1 " if IS_WIN;
- return " > " . $self->logfile . " 2>&1 &";
- }
- =head1 SEE ALSO
- Selenium::Chrome
- Selenium::Firefox
- Selenium::PhantomJS
- =cut
- 1;
|