CanStartBinary.pm 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371
  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 startup_timeout
  92. Optional: you can modify how long we will wait for the binary to start
  93. up. By default, we will start the binary and check the intended
  94. destination port for 10 seconds before giving up. If the machine
  95. you're using to run your browsers is slower or smaller, you may need
  96. to increase this timeout.
  97. The following:
  98. my $f = Selenium::Firefox->new(
  99. startup_timeout => 60
  100. );
  101. will wait up to 60 seconds for the firefox binary to respond on the
  102. proper port. To use this constructor option, you should specify a time
  103. in seconds as an integer, and it will be passed to the arguments
  104. section of a L<Selenium::Waiter/wait_until> subroutine call.
  105. =cut
  106. has startup_timeout => (
  107. is => 'lazy',
  108. default => sub { 10 }
  109. );
  110. =attr binary_mode
  111. Mostly intended for internal use, its builder coordinates all the side
  112. effects of interacting with the binary: locating the executable,
  113. finding an open port, setting up the environment, shelling out to
  114. start the binary, and ensuring that the webdriver is listening on the
  115. correct port.
  116. If all of the above steps pass, it will return truthy after
  117. instantiation. If any of them fail, it should return falsy and the
  118. class should attempt normal L<Selenium::Remote::Driver> behavior.
  119. =cut
  120. has 'binary_mode' => (
  121. is => 'lazy',
  122. init_arg => undef,
  123. builder => 1,
  124. predicate => 1
  125. );
  126. has 'try_binary' => (
  127. is => 'lazy',
  128. default => sub { 0 },
  129. trigger => sub {
  130. my ($self) = @_;
  131. $self->binary_mode if $self->try_binary;
  132. }
  133. );
  134. =attr window_title
  135. Intended for internal use: this will build us a unique title for the
  136. background binary process of the Webdriver. Then, when we're cleaning
  137. up, we know what the window title is that we're going to C<taskkill>.
  138. =cut
  139. has 'window_title' => (
  140. is => 'lazy',
  141. init_arg => undef,
  142. builder => sub {
  143. my ($self) = @_;
  144. my (undef, undef, $file) = File::Spec->splitpath( $self->binary );
  145. my $port = $self->port;
  146. return $file . ':' . $port;
  147. }
  148. );
  149. use constant IS_WIN => $^O eq 'MSWin32';
  150. sub BUILDARGS {
  151. # There's a bit of finagling to do to since we can't ensure the
  152. # attribute instantiation order. To decide whether we're going into
  153. # binary mode, we need the remote_server_addr and port. But, they're
  154. # both lazy and only instantiated immediately before S:R:D's
  155. # remote_conn attribute. Once remote_conn is set, we can't change it,
  156. # so we need the following order:
  157. #
  158. # parent: remote_server_addr, port
  159. # role: binary_mode (aka _build_binary_mode)
  160. # parent: remote_conn
  161. #
  162. # Since we can't force an order, we introduced try_binary which gets
  163. # decided during BUILDARGS to tip us off as to whether we should try
  164. # binary mode or not.
  165. my ( $class, %args ) = @_;
  166. if ( ! exists $args{remote_server_addr} && ! exists $args{port} ) {
  167. $args{try_binary} = 1;
  168. # Windows may throw a fit about invalid pointers if we try to
  169. # connect to localhost instead of 127.1
  170. $args{remote_server_addr} = '127.0.0.1';
  171. }
  172. else {
  173. $args{try_binary} = 0;
  174. $args{binary_mode} = 0;
  175. }
  176. return { %args };
  177. }
  178. sub _build_binary_mode {
  179. my ($self) = @_;
  180. # We don't know what to do without a binary driver to start up
  181. return unless $self->binary;
  182. # Either the user asked for 4444, or we couldn't find an open port
  183. my $port = $self->port + 0;
  184. return if $port == 4444;
  185. if ($self->isa('Selenium::Firefox')) {
  186. my @args = ($port);
  187. if ($self->has_firefox_profile) {
  188. push @args, $self->firefox_profile;
  189. }
  190. setup_firefox_binary_env(@args);
  191. }
  192. my $command = $self->_construct_command;
  193. system($command);
  194. my $success = wait_until { probe_port($port) } timeout => $self->startup_timeout;
  195. if ($success) {
  196. return 1;
  197. }
  198. else {
  199. die 'Unable to connect to the ' . $self->binary . ' binary on port ' . $port;
  200. }
  201. }
  202. sub shutdown_binary {
  203. my ($self) = @_;
  204. if ( $self->auto_close && defined $self->session_id ) {
  205. $self->quit();
  206. }
  207. if ($self->has_binary_mode && $self->binary_mode) {
  208. # Tell the binary itself to shutdown
  209. my $port = $self->port;
  210. my $ua = $self->ua;
  211. my $res = $ua->get('http://127.0.0.1:' . $port . '/wd/hub/shutdown');
  212. # Close the orphaned command windows on windows
  213. $self->shutdown_windows_binary;
  214. }
  215. }
  216. sub shutdown_windows_binary {
  217. my ($self) = @_;
  218. if (IS_WIN) {
  219. if ($self->isa('Selenium::Firefox')) {
  220. # FIXME: Blech, handle a race condition that kills the
  221. # driver before it's finished cleaning up its sessions. In
  222. # particular, when the perl process ends, it wants to
  223. # clean up the temp directory it created for the Firefox
  224. # profile. But, if the Firefox process is still running,
  225. # it will have a lock on the temp profile directory, and
  226. # perl will get upset. This "solution" is _very_ bad.
  227. sleep(2);
  228. # Firefox doesn't have a Driver/Session architecture - the
  229. # only thing running is Firefox itself, so there's no
  230. # other task to kill.
  231. return;
  232. }
  233. else {
  234. my $kill = 'taskkill /FI "WINDOWTITLE eq ' . $self->window_title . '" > nul 2>&1';
  235. system($kill);
  236. }
  237. }
  238. }
  239. sub DEMOLISH {
  240. my ($self, $in_gd) = @_;
  241. # if we're in global destruction, all bets are off.
  242. return if $in_gd;
  243. $self->shutdown_binary;
  244. };
  245. sub _construct_command {
  246. my ($self) = @_;
  247. my $executable = $self->binary;
  248. # Executable path names may have spaces
  249. $executable = '"' . $executable . '"';
  250. # The different binaries take different arguments for proper setup
  251. $executable .= $self->_binary_args;
  252. # Handle Windows vs Unix discrepancies for invoking shell commands
  253. my ($prefix, $suffix) = ($self->_cmd_prefix, $self->_cmd_suffix);
  254. return join(' ', ($prefix, $executable, $suffix) );
  255. }
  256. sub _cmd_prefix {
  257. my ($self) = @_;
  258. if (IS_WIN) {
  259. my $prefix = 'start "' . $self->window_title . '"';
  260. # Let's minimize the command windows for the drivers that have
  261. # separate binaries - but let's not minimize the Firefox
  262. # window itself.
  263. if (! $self->isa('Selenium::Firefox')) {
  264. $prefix .= ' /MIN ';
  265. }
  266. return $prefix;
  267. }
  268. else {
  269. return '';
  270. }
  271. }
  272. sub _cmd_suffix {
  273. # TODO: allow users to specify whether & where they want driver
  274. # output to go
  275. if (IS_WIN) {
  276. return ' > /nul 2>&1 ';
  277. }
  278. else {
  279. return ' > /dev/null 2>&1 &';
  280. }
  281. }
  282. =head1 SEE ALSO
  283. Selenium::Chrome
  284. Selenium::Firefox
  285. Selenium::PhantomJS
  286. =cut
  287. 1;