1
0

CanStartBinary.pm 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512
  1. package Selenium::CanStartBinary;
  2. use strict;
  3. use warnings;
  4. # ABSTRACT: Teach a WebDriver how to start its own binary aka no JRE!
  5. use File::Spec;
  6. use Selenium::CanStartBinary::ProbePort qw/find_open_port_above find_open_port probe_port/;
  7. use Selenium::Firefox::Binary qw/setup_firefox_binary_env/;
  8. use Selenium::Waiter qw/wait_until/;
  9. use Moo::Role;
  10. =for Pod::Coverage *EVERYTHING*
  11. =head1 DESCRIPTION
  12. This role takes care of the details for starting up a Webdriver
  13. instance. It does not do any downloading or installation of any sort -
  14. you're still responsible for obtaining and installing the necessary
  15. binaries into your C<$PATH> for this role to find. You may be
  16. interested in L<Selenium::Chrome>, L<Selenium::Firefox>, or
  17. L<Selenium::PhantomJS> if you're looking for classes that already
  18. consume this role.
  19. The role determines whether or not it should try to do its own magic
  20. based on whether the consuming class is instantiated with a
  21. C<remote_server_addr> and/or C<port>.
  22. # We'll start up the Chrome binary for you
  23. my $chrome_via_binary = Selenium::Chrome->new;
  24. # Look for a selenium server running on 4444.
  25. my $chrome_via_server = Selenium::Chrome->new( port => 4444 );
  26. If they're missing, we assume the user wants to use a webdriver
  27. directly and act accordingly. We handle finding the proper associated
  28. binary (or you can specify it with L</binary>), figuring out what
  29. arguments it wants, setting up any necessary environments, and
  30. starting up the binary.
  31. There's a number of TODOs left over - namely Windows support is
  32. severely lacking, and we're pretty naive when we attempt to locate the
  33. executables on our own.
  34. In the following documentation, C<required> refers to when you're
  35. consuming the role, not the C<required> when you're instantiating a
  36. class that has already consumed the role.
  37. =attr binary
  38. Required: Specify the path to the executable in question, or the name
  39. of the executable for us to find via L<File::Which/which>.
  40. =cut
  41. requires 'binary';
  42. =attr binary_port
  43. Required: Specify a default port that for the webdriver binary to try
  44. to bind to. If that port is unavailable, we'll probe above that port
  45. until we find a valid one.
  46. =cut
  47. requires 'binary_port';
  48. =attr _binary_args
  49. Required: Specify the arguments that the particular binary needs in
  50. order to start up correctly. In particular, you may need to tell the
  51. binary about the proper port when we start it up, or that it should
  52. use a particular prefix to match up with the behavior of the Remote
  53. Driver server.
  54. If your binary doesn't need any arguments, just have the default be an
  55. empty string.
  56. =cut
  57. requires '_binary_args';
  58. =attr port
  59. The role will attempt to determine the proper port for us. Consuming
  60. roles should set a default port in L</binary_port> at which we will
  61. begin searching for an open port.
  62. Note that if we cannot locate a suitable L</binary>, port will be set
  63. to 4444 so we can attempt to look for a Selenium server at
  64. C<127.0.0.1:4444>.
  65. =cut
  66. has '_real_binary' => (
  67. is => 'lazy',
  68. builder => sub {
  69. my ($self) = @_;
  70. if ($self->_is_old_ff) {
  71. return $self->firefox_binary;
  72. }
  73. else {
  74. return $self->binary;
  75. }
  76. }
  77. );
  78. has '_is_old_ff' => (
  79. is => 'lazy',
  80. builder => sub {
  81. my ($self) = @_;
  82. return $self->isa('Selenium::Firefox') && !$self->marionette_enabled;
  83. }
  84. );
  85. has '+port' => (
  86. is => 'lazy',
  87. builder => sub {
  88. my ($self) = @_;
  89. if ($self->_real_binary) {
  90. if ($self->fixed_ports) {
  91. return find_open_port($self->binary_port);
  92. }
  93. else {
  94. return find_open_port_above($self->binary_port);
  95. }
  96. }
  97. else {
  98. return 4444
  99. }
  100. }
  101. );
  102. =attr fixed_ports
  103. Optional: By default, if binary_port and marionette_port are not free
  104. a higher free port is probed and acquired if possible, until a free one
  105. if found or a timeout is exceeded.
  106. my $driver1 = Selenium::Chrome->new;
  107. my $driver2 = Selenium::Chrome->new( port => 1234 );
  108. The default behavior can be overridden. In this case, only the default
  109. or given binary_port and marionette_port are probed, without probing
  110. higher ports. This ensures that either the default or given port will be
  111. assigned, or no port will be assigned at all.
  112. my $driver1 = Selenium::Chrome->new( fixed_ports => 1 );
  113. my $driver2 = Selenium::Chrome->new( port => 1234, fixed_ports => 1);
  114. =cut
  115. has 'fixed_ports' => (
  116. is => 'lazy',
  117. default => sub { 0 }
  118. );
  119. =attr custom_args
  120. Optional: If you want to pass additional options to the binary when it
  121. starts up, you can add that here. For example, if your binary accepts
  122. an argument on the command line like C<--log-path=/path/to/log>, and
  123. you'd like to specify that the binary uses that option, you could do:
  124. my $chrome = Selenium::Chrome->new(
  125. custom_args => '--log-path=/path/to/log'
  126. );
  127. To specify multiple arguments, just include them all in the string.
  128. =cut
  129. has custom_args => (
  130. is => 'lazy',
  131. predicate => 1,
  132. default => sub { '' }
  133. );
  134. has 'marionette_port' => (
  135. is => 'lazy',
  136. builder => sub {
  137. my ($self) = @_;
  138. if ($self->_is_old_ff) {
  139. return 0;
  140. }
  141. else {
  142. if ($self->fixed_ports) {
  143. return find_open_port($self->marionette_binary_port);
  144. }
  145. else {
  146. return find_open_port_above($self->marionette_binary_port);
  147. }
  148. }
  149. }
  150. );
  151. =attr startup_timeout
  152. Optional: you can modify how long we will wait for the binary to start
  153. up. By default, we will start the binary and check the intended
  154. destination port for 10 seconds before giving up. If the machine
  155. you're using to run your browsers is slower or smaller, you may need
  156. to increase this timeout.
  157. The following:
  158. my $f = Selenium::Firefox->new(
  159. startup_timeout => 60
  160. );
  161. will wait up to 60 seconds for the firefox binary to respond on the
  162. proper port. To use this constructor option, you should specify a time
  163. in seconds as an integer, and it will be passed to the arguments
  164. section of a L<Selenium::Waiter/wait_until> subroutine call.
  165. =cut
  166. has startup_timeout => (
  167. is => 'lazy',
  168. default => sub { 10 }
  169. );
  170. =attr binary_mode
  171. Mostly intended for internal use, its builder coordinates all the side
  172. effects of interacting with the binary: locating the executable,
  173. finding an open port, setting up the environment, shelling out to
  174. start the binary, and ensuring that the webdriver is listening on the
  175. correct port.
  176. If all of the above steps pass, it will return truthy after
  177. instantiation. If any of them fail, it should return falsy and the
  178. class should attempt normal L<Selenium::Remote::Driver> behavior.
  179. =cut
  180. has 'binary_mode' => (
  181. is => 'lazy',
  182. init_arg => undef,
  183. builder => 1,
  184. predicate => 1
  185. );
  186. has 'try_binary' => (
  187. is => 'lazy',
  188. default => sub { 0 },
  189. trigger => sub {
  190. my ($self) = @_;
  191. $self->binary_mode if $self->try_binary;
  192. }
  193. );
  194. =attr window_title
  195. Intended for internal use: this will build us a unique title for the
  196. background binary process of the Webdriver. Then, when we're cleaning
  197. up, we know what the window title is that we're going to C<taskkill>.
  198. =cut
  199. has 'window_title' => (
  200. is => 'lazy',
  201. init_arg => undef,
  202. builder => sub {
  203. my ($self) = @_;
  204. my (undef, undef, $file) = File::Spec->splitpath( $self->_real_binary );
  205. my $port = $self->port;
  206. return $file . ':' . $port;
  207. }
  208. );
  209. =attr command
  210. Intended for internal use: this read-only attribute is built by us,
  211. but it can be useful after instantiation to see exactly what command
  212. was run to start the webdriver server.
  213. my $f = Selenium::Firefox->new;
  214. say $f->_command;
  215. =cut
  216. has '_command' => (
  217. is => 'lazy',
  218. init_arg => undef,
  219. builder => sub {
  220. my ($self) = @_;
  221. return $self->_construct_command;
  222. }
  223. );
  224. use constant IS_WIN => $^O eq 'MSWin32';
  225. sub BUILDARGS {
  226. # There's a bit of finagling to do to since we can't ensure the
  227. # attribute instantiation order. To decide whether we're going into
  228. # binary mode, we need the remote_server_addr and port. But, they're
  229. # both lazy and only instantiated immediately before S:R:D's
  230. # remote_conn attribute. Once remote_conn is set, we can't change it,
  231. # so we need the following order:
  232. #
  233. # parent: remote_server_addr, port
  234. # role: binary_mode (aka _build_binary_mode)
  235. # parent: remote_conn
  236. #
  237. # Since we can't force an order, we introduced try_binary which gets
  238. # decided during BUILDARGS to tip us off as to whether we should try
  239. # binary mode or not.
  240. my ( undef, %args ) = @_;
  241. if ( ! exists $args{remote_server_addr} && ! exists $args{port} ) {
  242. $args{try_binary} = 1;
  243. # Windows may throw a fit about invalid pointers if we try to
  244. # connect to localhost instead of 127.1
  245. $args{remote_server_addr} = '127.0.0.1';
  246. }
  247. else {
  248. $args{try_binary} = 0;
  249. $args{binary_mode} = 0;
  250. }
  251. return { %args };
  252. }
  253. sub _build_binary_mode {
  254. my ($self) = @_;
  255. # We don't know what to do without a binary driver to start up
  256. return unless $self->_real_binary;
  257. # Either the user asked for 4444, or we couldn't find an open port
  258. my $port = $self->port + 0;
  259. return if $port == 4444;
  260. if( $self->fixed_ports && $port == 0 ){
  261. die 'port ' . $self->binary_port . ' is not free and have requested fixed ports';
  262. }
  263. $self->_handle_firefox_setup($port);
  264. system($self->_command);
  265. my $success = wait_until { probe_port($port) } timeout => $self->startup_timeout;
  266. if ($success) {
  267. return 1;
  268. }
  269. else {
  270. die 'Unable to connect to the ' . $self->_real_binary . ' binary on port ' . $port;
  271. }
  272. }
  273. sub _handle_firefox_setup {
  274. my ($self, $port) = @_;
  275. # This is a no-op for other browsers
  276. return unless $self->isa('Selenium::Firefox');
  277. my $user_profile = $self->has_firefox_profile
  278. ? $self->firefox_profile
  279. : 0;
  280. my $profile = setup_firefox_binary_env(
  281. $port,
  282. $self->marionette_port,
  283. $user_profile
  284. );
  285. if ($self->_is_old_ff) {
  286. # For non-geckodriver/non-marionette, we want to get rid of
  287. # the profile so that we don't accidentally zip it and encode
  288. # it down the line while Firefox is trying to read from it.
  289. $self->clear_firefox_profile if $self->has_firefox_profile;
  290. }
  291. else {
  292. # For geckodriver/marionette, we keep the enhanced profile around because
  293. # we need to send it to geckodriver as a zipped b64-encoded
  294. # directory.
  295. $self->firefox_profile($profile);
  296. }
  297. }
  298. sub shutdown_binary {
  299. my ($self) = @_;
  300. if ( $self->auto_close && defined $self->session_id ) {
  301. $self->quit();
  302. }
  303. if ($self->has_binary_mode && $self->binary_mode) {
  304. # Tell the binary itself to shutdown
  305. my $port = $self->port;
  306. my $ua = $self->ua;
  307. $ua->get('http://127.0.0.1:' . $port . '/wd/hub/shutdown');
  308. # Close the orphaned command windows on windows
  309. $self->shutdown_windows_binary;
  310. $self->shutdown_unix_binary;
  311. }
  312. }
  313. sub shutdown_unix_binary {
  314. my ($self) = @_;
  315. if (!IS_WIN) {
  316. my $cmd = "lsof -t -i :".$self->port();
  317. my $pid = `$cmd`;
  318. chomp $pid;
  319. if ($pid) {
  320. print "Killing Driver PID $pid listening on port ".$self->port."...\n";
  321. eval { kill 'KILL', $pid };
  322. warn "Could not kill driver process! you may have to clean up manually." if $@;
  323. }
  324. }
  325. }
  326. sub shutdown_windows_binary {
  327. my ($self) = @_;
  328. if (IS_WIN) {
  329. if ($self->_is_old_ff) {
  330. # FIXME: Blech, handle a race condition that kills the
  331. # driver before it's finished cleaning up its sessions. In
  332. # particular, when the perl process ends, it wants to
  333. # clean up the temp directory it created for the Firefox
  334. # profile. But, if the Firefox process is still running,
  335. # it will have a lock on the temp profile directory, and
  336. # perl will get upset. This "solution" is _very_ bad.
  337. sleep(2);
  338. # Firefox doesn't have a Driver/Session architecture - the
  339. # only thing running is Firefox itself, so there's no
  340. # other task to kill.
  341. return;
  342. }
  343. system('taskkill /FI "WINDOWTITLE eq ' . $self->window_title . '" > nul 2>&1');
  344. }
  345. }
  346. sub DEMOLISH {
  347. my ($self, $in_gd) = @_;
  348. # if we're in global destruction, all bets are off.
  349. return if $in_gd;
  350. $self->shutdown_binary;
  351. }
  352. sub _construct_command {
  353. my ($self) = @_;
  354. my $executable = $self->_real_binary;
  355. # Executable path names may have spaces
  356. $executable = '"' . $executable . '"';
  357. # The different binaries take different arguments for proper setup
  358. $executable .= $self->_binary_args;
  359. if ($self->has_custom_args) {
  360. $executable .= ' ' . $self->custom_args;
  361. }
  362. # Handle Windows vs Unix discrepancies for invoking shell commands
  363. my ($prefix, $suffix) = ($self->_cmd_prefix, $self->_cmd_suffix);
  364. return join(' ', ($prefix, $executable, $suffix) );
  365. }
  366. sub _cmd_prefix {
  367. my ($self) = @_;
  368. my $prefix = '';
  369. if (IS_WIN) {
  370. $prefix = 'start "' . $self->window_title . '"';
  371. if ($self->_is_old_ff) {
  372. # For older versions of Firefox that run without
  373. # marionette, the command we're running actually starts up
  374. # the browser itself, so we don't want to minimize it.
  375. return $prefix;
  376. }
  377. else {
  378. # If we're firefox with marionette, or any other browser,
  379. # the command we're running is the driver, and we don't
  380. # need want the command window in the foreground.
  381. return $prefix . ' /MIN ';
  382. }
  383. }
  384. return $prefix;
  385. }
  386. sub _cmd_suffix {
  387. # TODO: allow users to specify whether & where they want driver
  388. # output to go
  389. if (IS_WIN) {
  390. return ' > /nul 2>&1 ';
  391. }
  392. else {
  393. return ' > /dev/null 2>&1 &';
  394. }
  395. }
  396. =head1 SEE ALSO
  397. Selenium::Chrome
  398. Selenium::Firefox
  399. Selenium::PhantomJS
  400. =cut
  401. 1;