CanStartBinary.pm 9.5 KB

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