1
0

CanStartBinary.pm 11 KB

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