1
0

CanStartBinary.pm 13 KB

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