1
0

CanStartBinary.pm 8.6 KB

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