CanStartBinary.pm 6.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253
  1. package Selenium::CanStartBinary;
  2. # ABSTRACT: Teach a WebDriver how to start its own binary aka no JRE!
  3. use File::Which qw/which/;
  4. use IO::Socket::INET;
  5. use Selenium::Waiter qw/wait_until/;
  6. use Selenium::Firefox::Binary qw/firefox_path setup_firefox_binary_env/;
  7. use Selenium::Firefox::Profile;
  8. use Moo::Role;
  9. =head1 NAME
  10. CanStartBinary - Role that a Selenium::Remote::Driver can consume to start a binary
  11. =head1 SYNOPSIS
  12. package ChromeDriver {
  13. use Moo;
  14. with 'Selenium::CanStartBinary';
  15. extends 'Selenium::Remote::Driver';
  16. has 'binary_name' => ( is => 'ro', default => 'chromedriver' );
  17. has 'binary_port' => ( is => 'ro', default => 9515 );
  18. 1
  19. };
  20. my $chrome_via_binary = ChromeDriver->new;
  21. =head1 DESCRIPTION
  22. This role takes care of the details for starting up a Webdriver
  23. instance. It does not do any downloading or installation of any sort -
  24. you're still responsible for obtaining and installing the necessary
  25. binaries into your C<$PATH> for this role to find.
  26. The role determines whether or not it should try to do its own magic
  27. based on whether or not the consuming class is instantiated with a
  28. C<remote_server_addr> and/or C<port>. If they're missing, we assume
  29. the user wants to use the Webdrivers directly and act
  30. accordingly. We'll go find the proper associated binary (or you can
  31. specify it with L</binary_path>), figure out what arguments it wants,
  32. set up any necessary environments, and start up the binary.
  33. There's a number of TODOs left over - namely Windows support is
  34. severely lacking, and we're pretty naive when we attempt to locate the
  35. executables on our own. You may be well served in specifying the paths
  36. to the webdriver in question yourself, if we can't figure it out.
  37. =attr binary_path
  38. Optional: specify the path to the executable in question. If you don't
  39. specify anything, we use L<File::Which/which> and take our best guess
  40. as to where the proper executable might be. If the expected executable
  41. is in your C<$PATH>, you shouldn't have to use this attribute.
  42. As always, make sure _not_ to specify the C<remote_server_addr> and
  43. C<port> when instantiating your class, or we'll have no choice but to
  44. assume you're running a Remote Webdriver instance.
  45. =cut
  46. has 'binary_path' => (
  47. is => 'lazy',
  48. default => sub { '' },
  49. predicate => 1
  50. );
  51. has 'binary_mode' => (
  52. is => 'lazy',
  53. init_arg => undef,
  54. builder => 1,
  55. predicate => 1
  56. );
  57. has 'try_binary' => (
  58. is => 'lazy',
  59. default => sub { 0 },
  60. trigger => sub {
  61. my ($self) = @_;
  62. $self->binary_mode if $self->try_binary;
  63. }
  64. );
  65. sub BUILDARGS {
  66. # There's a bit of finagling to do to since we can't ensure the
  67. # attribute instantiation order. To decide whether we're going into
  68. # binary mode, we need the remote_server_addr and port. But, they're
  69. # both lazy and only instantiated immediately before S:R:D's
  70. # remote_conn attribute. Once remote_conn is set, we can't change it,
  71. # so we need the following order:
  72. #
  73. # parent: remote_server_addr, port
  74. # role: binary_mode (aka _build_binary_mode)
  75. # parent: remote_conn
  76. #
  77. # Since we can't force an order, we introduced try_binary which gets
  78. # decided during BUILDARGS to tip us off as to whether we should try
  79. # binary mode or not.
  80. my ( $class, %args ) = @_;
  81. if ( ! exists $args{remote_server_addr} && ! exists $args{port} ) {
  82. $args{try_binary} = 1;
  83. # Windows may throw a fit about invalid pointers if we try to
  84. # connect to localhost instead of 127.1
  85. $args{remote_server_addr} = '127.0.0.1';
  86. }
  87. return { %args };
  88. }
  89. sub _build_binary_mode {
  90. my ($self) = @_;
  91. my $port = $self->start_binary_on_port(
  92. $self->binary_name,
  93. $self->binary_port
  94. );
  95. $self->port($port);
  96. return 1;
  97. }
  98. sub probe_port {
  99. my ($port) = @_;
  100. return IO::Socket::INET->new(
  101. PeerAddr => '127.0.0.1',
  102. PeerPort => $port,
  103. Timeout => 3
  104. );
  105. }
  106. sub start_binary_on_port {
  107. my ($self, $process, $port) = @_;
  108. my $executable = $self->_find_executable($process);
  109. $port = _find_open_port_above($port);
  110. if ($process eq 'firefox') {
  111. setup_firefox_binary_env($port);
  112. }
  113. my $command = _construct_command($executable, $port);
  114. system($command);
  115. my $success = wait_until { probe_port($port) } timeout => 10;
  116. if ($success) {
  117. return $port;
  118. }
  119. else {
  120. die 'Unable to connect to the ' . $executable . ' binary on port ' . $port;
  121. }
  122. }
  123. sub shutdown_binary {
  124. my ($self) = @_;
  125. if ($self->has_binary_mode && $self->binary_mode) {
  126. my $port = $self->port;
  127. my $ua = $self->ua;
  128. $ua->get('127.0.0.1:' . $port . '/wd/hub/shutdown');
  129. }
  130. }
  131. sub _find_executable {
  132. my ($self, $binary) = @_;
  133. if ($self->has_binary_path) {
  134. if (-x $self->binary_path) {
  135. return $self->binary_path;
  136. }
  137. else {
  138. die 'The binary at "' . $self->binary_path . '" is not executable. Fix the path or chmod +x it as needed.';
  139. }
  140. }
  141. if ($binary eq 'firefox') {
  142. return firefox_path();
  143. }
  144. else {
  145. my $executable = which($binary);
  146. if (not defined $executable) {
  147. warn qq(Unable to find the $binary binary in your \$PATH. We'll try falling back to standard Remote Driver);
  148. }
  149. else {
  150. return $executable;
  151. }
  152. }
  153. }
  154. sub _construct_command {
  155. my ($executable, $port) = @_;
  156. my %args;
  157. if ($executable =~ /chromedriver(\.exe)?$/i) {
  158. %args = (
  159. port => $port,
  160. 'url-base' => 'wd/hub'
  161. );
  162. }
  163. elsif ($executable =~ /phantomjs(\.exe)?$/i) {
  164. %args = (
  165. webdriver => '127.0.0.1:' . $port
  166. );
  167. }
  168. elsif ($executable =~ /firefox/i) {
  169. $executable .= ' -no-remote ';
  170. }
  171. my @args = map { '--' . $_ . '=' . $args{$_} } keys %args;
  172. # Handle Windows vs Unix discrepancies for invoking shell commands
  173. my ($prefix, $suffix) = (_command_prefix(), _command_suffix());
  174. return join(' ', ($prefix, $executable, @args, $suffix) );
  175. }
  176. sub _command_prefix {
  177. if ($^O eq 'MSWin32') {
  178. return 'start /MAX '
  179. }
  180. else {
  181. return '';
  182. }
  183. }
  184. sub _command_suffix {
  185. if ($^O eq 'MSWin32') {
  186. return ' > /nul 2>&1 ';
  187. }
  188. else {
  189. # TODO: allow users to specify whether & where they want
  190. # driver output to go
  191. return ' > /dev/null 2>&1 &';
  192. }
  193. }
  194. sub _find_open_port_above {
  195. my ($port) = @_;
  196. my $free_port = wait_until {
  197. if ( probe_port($port) ) {
  198. $port++;
  199. return 0;
  200. }
  201. else {
  202. return $port;
  203. }
  204. };
  205. return $free_port;
  206. }
  207. 1;