CanStartBinary.pm 7.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299
  1. package Selenium::CanStartBinary;
  2. # ABSTRACT: Teach a WebDriver how to start its own binary aka no JRE!
  3. use File::Spec;
  4. use Selenium::CanStartBinary::ProbePort qw/find_open_port_above probe_port/;
  5. use Selenium::Firefox::Binary qw/setup_firefox_binary_env/;
  6. use Selenium::Waiter qw/wait_until/;
  7. use Moo::Role;
  8. =head1 NAME
  9. CanStartBinary - Role that a Selenium::Remote::Driver can consume to start a binary
  10. =head1 SYNOPSIS
  11. package ChromeDriver {
  12. use Moo;
  13. with 'Selenium::CanStartBinary';
  14. extends 'Selenium::Remote::Driver';
  15. has 'binary' => ( is => 'ro', default => 'chromedriver' );
  16. has 'binary_port' => ( is => 'ro', default => 9515 );
  17. 1
  18. };
  19. my $chrome_via_binary = ChromeDriver->new;
  20. =head1 DESCRIPTION
  21. This role takes care of the details for starting up a Webdriver
  22. instance. It does not do any downloading or installation of any sort -
  23. you're still responsible for obtaining and installing the necessary
  24. binaries into your C<$PATH> for this role to find.
  25. The role determines whether or not it should try to do its own magic
  26. based on whether or not the consuming class is instantiated with a
  27. C<remote_server_addr> and/or C<port>. If they're missing, we assume
  28. the user wants to use the Webdrivers directly and act
  29. accordingly. We'll go find the proper associated binary (or you can
  30. specify it with L</binary_path>), figure out what arguments it wants,
  31. set up any necessary environments, and start up the binary.
  32. There's a number of TODOs left over - namely Windows support is
  33. severely lacking, and we're pretty naive when we attempt to locate the
  34. executables on our own.
  35. In the following documentation, C<required> refers to when you're
  36. consuming the role, not the C<required> when you're instantiating a
  37. class that has already consumed the role.
  38. =attr binary
  39. Required: Specify the path to the executable in question, or the name
  40. of the executable for us to find via L<File::Which/which>.
  41. =cut
  42. requires 'binary';
  43. =attr binary_port
  44. Required: Specify a default port that for the webdriver binary to try
  45. to bind to. If that port is unavailable, we'll probe above that port
  46. until we find a valid one.
  47. =cut
  48. requires 'binary_port';
  49. =attr port
  50. The role will attempt to determine the proper port for us. Consuming
  51. roles should set a default port in L</binary_port> at which we will
  52. begin searching for an open port.
  53. Note that if we cannot locate a suitable L</binary>, port will be set
  54. to 4444 so we can attempt to look for a Selenium server at
  55. C<127.0.0.1:4444>.
  56. =cut
  57. has 'port' => (
  58. is => 'lazy',
  59. builder => sub {
  60. my ($self) = @_;
  61. if ($self->binary) {
  62. return find_open_port_above($self->binary_port);
  63. }
  64. else {
  65. return '4444'
  66. }
  67. }
  68. );
  69. =attr binary_mode
  70. Mostly intended for internal use, its builder coordinates all the side
  71. effects of interacting with the binary: locating the executable,
  72. finding an open port, setting up the environment, shelling out to
  73. start the binary, and ensuring that the webdriver is listening on the
  74. correct port.
  75. If all of the above steps pass, it will return truthy after
  76. instantiation. If any of them fail, it should return falsy and the
  77. class should attempt normal L<Selenium::Remote::Driver> behavior.
  78. =cut
  79. has 'binary_mode' => (
  80. is => 'lazy',
  81. init_arg => undef,
  82. builder => 1,
  83. predicate => 1
  84. );
  85. has 'try_binary' => (
  86. is => 'lazy',
  87. default => sub { 0 },
  88. trigger => sub {
  89. my ($self) = @_;
  90. $self->binary_mode if $self->try_binary;
  91. }
  92. );
  93. =attr window_title
  94. Intended for internal use: this will build us a unique title for the
  95. background binary process of the Webdriver. Then, when we're cleaning
  96. up, we know what the window title is that we're going to C<taskkill>.
  97. =cut
  98. has 'window_title' => (
  99. is => 'lazy',
  100. init_arg => undef,
  101. builder => sub {
  102. my ($self) = @_;
  103. my (undef, undef, $file) = File::Spec->splitpath( $self->binary );
  104. my $port = $self->port;
  105. return $file . ':' . $port;
  106. }
  107. );
  108. use constant IS_WIN => $^O eq 'MSWin32';
  109. sub BUILDARGS {
  110. # There's a bit of finagling to do to since we can't ensure the
  111. # attribute instantiation order. To decide whether we're going into
  112. # binary mode, we need the remote_server_addr and port. But, they're
  113. # both lazy and only instantiated immediately before S:R:D's
  114. # remote_conn attribute. Once remote_conn is set, we can't change it,
  115. # so we need the following order:
  116. #
  117. # parent: remote_server_addr, port
  118. # role: binary_mode (aka _build_binary_mode)
  119. # parent: remote_conn
  120. #
  121. # Since we can't force an order, we introduced try_binary which gets
  122. # decided during BUILDARGS to tip us off as to whether we should try
  123. # binary mode or not.
  124. my ( $class, %args ) = @_;
  125. if ( ! exists $args{remote_server_addr} && ! exists $args{port} ) {
  126. $args{try_binary} = 1;
  127. # Windows may throw a fit about invalid pointers if we try to
  128. # connect to localhost instead of 127.1
  129. $args{remote_server_addr} = '127.0.0.1';
  130. }
  131. return { %args };
  132. }
  133. sub _build_binary_mode {
  134. my ($self) = @_;
  135. my $executable = $self->binary;
  136. return unless $executable;
  137. my $port = $self->port;
  138. return unless $port != 4444;
  139. if ($self->isa('Selenium::Firefox')) {
  140. setup_firefox_binary_env($port);
  141. }
  142. my $command = $self->_construct_command($executable, $port);
  143. system($command);
  144. my $success = wait_until { probe_port($port) } timeout => 10;
  145. if ($success) {
  146. return 1;
  147. }
  148. else {
  149. die 'Unable to connect to the ' . $executable . ' binary on port ' . $port;
  150. }
  151. }
  152. sub shutdown_binary {
  153. my ($self) = @_;
  154. # TODO: Allow user to keep browser open after test
  155. $self->quit;
  156. if ($self->has_binary_mode && $self->binary_mode) {
  157. my $port = $self->port;
  158. my $ua = $self->ua;
  159. $ua->get('127.0.0.1:' . $port . '/wd/hub/shutdown');
  160. # Close the additional command windows on windows
  161. if (IS_WIN) {
  162. # Blech, handle a race condition that kills the driver
  163. # before it's finished cleaning up its sessions
  164. sleep(1);
  165. $self->shutdown_windows_binary;
  166. }
  167. }
  168. }
  169. sub shutdown_windows_binary {
  170. my ($self) = @_;
  171. # Firefox doesn't have a Driver/Session architecture - the only
  172. # thing running is Firefox itself, so there's no other task to
  173. # kill.
  174. return if $self->isa('Selenium::Firefox');
  175. my $kill = 'taskkill /FI "WINDOWTITLE eq ' . $self->window_title . '"';
  176. system($kill);
  177. }
  178. before DEMOLISH => sub {
  179. my ($self) = @_;
  180. $self->shutdown_binary;
  181. };
  182. sub DEMOLISH { };
  183. sub _construct_command {
  184. my ($self, $executable, $port) = @_;
  185. # Handle spaces in executable path names
  186. $executable = '"' . $executable . '"';
  187. my %args;
  188. if ($executable =~ /chromedriver(\.exe)?"$/i) {
  189. %args = (
  190. port => $port,
  191. 'url-base' => 'wd/hub'
  192. );
  193. }
  194. elsif ($executable =~ /phantomjs(\.exe)?"$/i) {
  195. %args = (
  196. webdriver => '127.0.0.1:' . $port
  197. );
  198. }
  199. elsif ($executable =~ /firefox(-bin|\.exe)"$/i) {
  200. $executable .= ' -no-remote ';
  201. }
  202. my @args = map { '--' . $_ . '=' . $args{$_} } keys %args;
  203. # Handle Windows vs Unix discrepancies for invoking shell commands
  204. my ($prefix, $suffix) = ($self->_cmd_prefix, $self->_cmd_suffix);
  205. return join(' ', ($prefix, $executable, @args, $suffix) );
  206. }
  207. sub _cmd_prefix {
  208. my ($self) = @_;
  209. if (IS_WIN) {
  210. return 'start "' . $self->window_title . '" /MIN '
  211. }
  212. else {
  213. return '';
  214. }
  215. }
  216. sub _cmd_suffix {
  217. # TODO: allow users to specify whether & where they want driver
  218. # output to go
  219. if (IS_WIN) {
  220. return ' > /nul 2>&1 ';
  221. }
  222. else {
  223. return ' > /dev/null 2>&1 &';
  224. }
  225. }
  226. =head1 SEE ALSO
  227. Selenium::Chrome
  228. Selenium::Firefox
  229. Selenium::PhantomJS
  230. =cut
  231. 1;