File Coverage

blib/lib/Playwright.pm
Criterion Covered Total %
statement 121 254 47.6
branch 15 78 19.2
condition 14 50 28.0
subroutine 29 42 69.0
pod 8 8 100.0
total 187 432 43.2


line stmt bran cond sub pod time code
1             package Playwright;
2             $Playwright::VERSION = '1.551';
3 1     1   541600 use strict;
  1         3  
  1         46  
4 1     1   6 use warnings;
  1         2  
  1         66  
5              
6             #ABSTRACT: Perl client for Playwright
7 1     1   53 use 5.006;
  1         4  
8 1     1   11 use v5.28.0; # Before 5.006, v5.10.0 would not be understood.
  1         19  
9              
10 1     1   8 use constant IS_WIN => $^O eq 'MSWin32';
  1         2  
  1         101  
11              
12 1     1   862 use File::ShareDir();
  1         45589  
  1         47  
13 1     1   11 use File::Basename();
  1         2  
  1         21  
14 1     1   8 use Cwd();
  1         2  
  1         19  
15 1     1   788 use LWP::UserAgent();
  1         58191  
  1         26  
16 1     1   389 use Sub::Install();
  1         1530  
  1         26  
17 1     1   585 use Net::EmptyPort();
  1         28371  
  1         27  
18 1     1   6 use JSON::MaybeXS();
  1         2  
  1         12  
19 1     1   441 use File::Which();
  1         990  
  1         18  
20 1     1   696 use File::Temp();
  1         8014  
  1         54  
21 1     1   549 use Capture::Tiny qw{capture_merged capture_stderr};
  1         3972  
  1         47  
22 1     1   5 use Carp qw{confess};
  1         2  
  1         33  
23              
24 1     1   469 use Playwright::Base();
  1         2  
  1         22  
25 1     1   4 use Playwright::Util();
  1         1  
  1         11  
26              
27             # Stuff closet full of skeletons at BEGIN time
28 1     1   387 use Playwright::ModuleList();
  1         5  
  1         37  
29              
30 1     1   8 no warnings 'experimental';
  1         2  
  1         61  
31 1     1   6 use feature qw{signatures};
  1         2  
  1         976  
32              
33             our ( $spec, $server_bin, $node_bin, %mapper );
34              
35             sub _check_node {
36              
37             # Check that node is installed
38 1     1   3799 $node_bin = File::Which::which('node');
39 1 50 33     22 confess("node must exist, be in your PATH and executable")
40             unless $node_bin && -x $node_bin;
41              
42             # Make sure it's possible to start the server
43 0         0 $server_bin = File::Which::which('playwright_server');
44              
45             # If it's not in $PATH, it should be in ../bin
46 0   0     0 $server_bin //= Playwright::Util::find_playwright_server();
47              
48 0 0 0     0 confess(
49             "Can't locate playwright_server!
50             Please ensure it is installed in your PATH.
51             If you installed this module from CPAN, it should already be."
52             ) unless $server_bin && -x $server_bin;
53              
54 0         0 $server_bin = Cwd::abs_path($server_bin);
55              
56             # Attempt to start the server. If we can't do this, we almost certainly have dependency issues.
57 0         0 my $output = '';
58 0         0 if (IS_WIN) {
59             $output = 'OK';
60             }
61             else {
62             ($output) =
63 0     0   0 capture_merged { system( $node_bin, $server_bin, '--check' ) };
  0         0  
64             }
65 0 0       0 return if $output =~ m/OK/;
66              
67 0 0       0 warn $output if $output;
68              
69 0         0 confess(
70             "playwright_server could not run successfully.
71             See the above error message for why.
72             It's likely to be unmet dependencies, or a NODE_PATH issue.
73              
74             Install of node dependencies must be done manually.
75             Run the following:
76              
77             npm i express playwright uuid
78             sudo npx playwright install-deps
79             export NODE_PATH=\"\$(pwd)/node_modules\".
80              
81             If you still experience issues, run the following:
82              
83             NODE_DEBUG=module playwright_server --check
84              
85             This should tell you why node can't find the deps you have installed.
86             "
87             );
88              
89             }
90              
91             sub _build_classes {
92 0     0   0 foreach my $class ( keys(%$spec) ) {
93             $mapper{$class} = sub {
94 0     0   0 my ( $self, $res ) = @_;
95 0         0 my $class = "Playwright::$class";
96             return $class->new(
97             handle => $self,
98             id => $res->{_guid},
99 0         0 type => $class,
100             parent => $self,
101             );
102 0         0 };
103             }
104             }
105              
106             sub BEGIN {
107 1     1   5 our $SKIP_BEGIN;
108 1 50       3548 _check_node() unless $SKIP_BEGIN;
109             }
110              
111 2     2 1 7176 sub new ( $class, %options ) {
  2         5  
  2         6  
  2         3  
112              
113             #XXX yes, this is a race, so we need retries in _start_server
114 2   33     13 my $port = $options{port} // Net::EmptyPort::empty_port();
115 2   50     17 my $cdp_uri = $options{cdp_uri} // '';
116 2   100     8 my $timeout = $options{timeout} // 30;
117 2   50     13 $options{maxrequest} //= '100kb';
118             my $cleanup =
119 2 50 33     19 ( $options{cleanup} // !( $options{port} || $options{host} ) ) ? 1 : 0;
      33        
120             my $self = bless(
121             {
122             ua => $options{ua} // LWP::UserAgent->new(),
123             host => $options{host} // 'localhost',
124             port => $port,
125             cdp_uri => $cdp_uri,
126             debug => $options{debug},
127             cleanup => $cleanup,
128             pid => $options{host} ? "REUSE" : _start_server(
129 2 50 66     20 $port, $cdp_uri, $timeout, $options{debug}, $cleanup
      50        
      50        
130             ),
131             parent => $$ // 'bogus', # Oh lawds, this can be undef sometimes
132             timeout => $timeout,
133             },
134             $class,
135             );
136              
137 2         53 $self->_check_and_build_spec();
138 2         10 _build_classes();
139              
140             # Control things about the environment we start pw in
141 2         22 $ENV{MAX_REQUEST_SIZE} = $options{maxrequest};
142              
143 2         14 return $self;
144             }
145              
146 2     2   8784 sub _check_and_build_spec ($self) {
  2         5  
  2         21  
147 2 100       18 return $spec if ref $spec eq 'HASH';
148              
149             $spec =
150             Playwright::Util::request( 'GET', 'spec', $self->{host}, $self->{port},
151 1         8 $self->{ua}, );
152              
153 1 50       498 confess(
154             "Could not retrieve Playwright specification. Check that your playwright installation is correct and complete."
155             ) unless ref $spec eq 'HASH';
156 0         0 return $spec;
157             }
158              
159 1     1 1 2580 sub launch ( $self, %args ) {
  1         4  
  1         3  
  1         2  
160              
161             Playwright::Base::_coerce(
162             $spec->{BrowserType}{members},
163 1         10 args => [ \%args ],
164             command => 'launch'
165             );
166 1         5 delete $args{command};
167              
168             my $msg = Playwright::Util::request(
169             'POST', 'session', $self->{host}, $self->{port}, $self->{ua},
170             type => delete $args{type},
171 1         11 args => [ \%args ]
172             );
173              
174             return $Playwright::mapper{ $msg->{_type} }->( $self, $msg )
175             if ( ref $msg eq 'HASH' )
176             && $msg->{_type}
177 1 0 33     9 && exists $Playwright::mapper{ $msg->{_type} };
      0        
178 1         7 return $msg;
179             }
180              
181 0     0 1 0 sub server ( $self, %args ) {
  0         0  
  0         0  
  0         0  
182             return Playwright::Util::request(
183             'POST', 'server', $self->{host}, $self->{port}, $self->{ua},
184             object => $args{browser}{guid},
185             command => $args{command},
186 0         0 );
187             }
188              
189 2     2 1 3326 sub await ( $self, $promise ) {
  2         5  
  2         5  
  2         4  
190 2         7 my $obj = Playwright::Util::await($promise);
191              
192 2 100       19 return $obj unless $obj->{_type};
193 1         3 my $class = "Playwright::$obj->{_type}";
194             return $class->new(
195             type => $obj->{_type},
196             id => $obj->{_guid},
197 1         9 handle => $self
198             );
199             }
200              
201 0     0 1 0 sub pusht ( $object, $timeout, $navigation = 0 ) {
  0         0  
  0         0  
  0         0  
  0         0  
202 0   0     0 $object->{timeouts} //= [];
203 0         0 push( @{ $object->{timeouts} }, $timeout );
  0         0  
204 0 0       0 return $object->setDefaultNavigationTimeout($timeout) if $navigation;
205 0         0 return $object->setDefaultTimeout($timeout);
206             }
207              
208 0     0 1 0 sub popt ( $object, $navigation = 0 ) {
  0         0  
  0         0  
  0         0  
209 0   0     0 $object->{timeouts} //= [];
210 0   0     0 my $last_timeout = pop( @{ $object->{timeouts} } ) // 1000;
  0         0  
211 0 0       0 return $object->setDefaultNavigationTimeout($last_timeout) if $navigation;
212 0         0 return $object->setDefaultTimeout($last_timeout);
213             }
214              
215 0     0 1 0 sub try_until ( $object, $method, @args ) {
  0         0  
  0         0  
  0         0  
  0         0  
216 0         0 my ( $ctr, $result, $timeout ) = (0);
217 0 0       0 $timeout = $object->{timeouts}[-1] if ref $object->{timeouts} eq 'ARRAY';
218 0 0       0 $timeout = $timeout / 1000 if $timeout;
219 0   0     0 $timeout //= 1;
220 0         0 while ( !$result ) {
221 0         0 $result = $object->$method(@args);
222 0 0       0 last if $result;
223 0         0 sleep 1;
224 0         0 $ctr++;
225 0 0       0 last if $ctr >= $timeout;
226             }
227 0         0 return $result;
228             }
229              
230 1     1 1 2 sub quit ($self) {
  1         3  
  1         2  
231              
232             # Prevent double destroy after quit()
233 1 50       9 return if $self->{killed};
234              
235             # Prevent destructor from firing in child processes so we can do things like async()
236             # This should also prevent the waitpid below from deadlocking due to two processes waiting on the same pid.
237 1   50     11 my $ppid = $$ // 'hokum'
238             ; # If $$ is undef both here and in the parent, let's just keep going
239 1 50       5 return unless $ppid == $self->{parent};
240              
241             # Prevent destructor from firing in the event the caller instructs it to not fire
242 1 50       4 return unless $self->{cleanup};
243              
244             # Make sure we don't mash the exit code of things like prove
245 1         5 local $?;
246              
247 1         3 $self->{killed} = 1;
248 1 50       4 print "Attempting to terminate server process...\n" if $self->{debug};
249              
250             # Best effort to whack this, we can't make guarantees during global destruction
251             eval {
252             capture_merged {
253             Playwright::Util::request( 'GET', 'shutdown', $self->{host},
254             $self->{port}, $self->{ua} )
255 1     0   67 };
  0         0  
256 1 50       8 } if $self->{ua};
257              
258 1         2552 return $self->_kill_playwright_server_windows() if IS_WIN;
259              
260             # 0 is always WCONTINUED, 1 is always WNOHANG, and POSIX is an expensive import
261             # When 0 is returned, the process is still active, so it needs more persuasion
262 1         7 foreach ( 0 .. 3 ) {
263 1 50       27 return unless waitpid( $self->{pid}, 1 ) == 0;
264 0         0 sleep 1;
265             }
266              
267             # Advanced persuasion
268 0 0       0 print "Forcibly terminating server process...\n" if $self->{debug};
269 0         0 kill( 'TERM', $self->{pid} );
270              
271             #XXX unfortunately I can't just do a SIGALRM, because blocking system calls can't be intercepted on win32
272 0         0 foreach ( 0 .. $self->{timeout} ) {
273 0 0       0 return unless waitpid( $self->{pid}, 1 ) == 0;
274 0         0 sleep 1;
275             }
276 0         0 warn "Could not shut down playwright server!";
277 0         0 return;
278             }
279              
280 1     1   2642 sub DESTROY ($self) {
  1         3  
  1         3  
281 1         4 $self->quit();
282             }
283              
284 0     0     sub _wait_port ( $port, $timeout, $debug ) {
  0            
  0            
  0            
  0            
285              
286             #XXX unusedvars is wigging
287 0           $debug = $debug;
288 0           my $result;
289 0           $result = $result;
290              
291             # Check if the port is already live, and short-circuit if this is the case.
292 0           if (IS_WIN) {
293             for ( 0 .. $timeout ) {
294             $result = qx{netstat -na | findstr "$port"};
295             print "Waiting on port $port: $result\n" if $debug;
296             last if $result;
297             sleep 1;
298             }
299             return !!$result;
300             }
301 0           return Net::EmptyPort::wait_port( $port, $timeout );
302             }
303              
304 0     0     sub _start_server ( $port, $cdp_uri, $timeout, $debug, $cleanup ) {
  0            
  0            
  0            
  0            
  0            
  0            
305 0 0         $debug = $debug ? '--debug' : '';
306              
307             # Check if the port is already live, and short-circuit if this is the case.
308 0 0         if ( _wait_port( $port, 1, $debug ) ) {
309 0 0         print "Re-using playwright server on port $port...\n" if $debug;
310              
311             # Set the PID as something bogus, we don't really care as we won't kill it
312 0           return "REUSE";
313             }
314              
315             # On windows, the args will have to be handled slightly differently.
316 0           my @args;
317 0           if (IS_WIN) {
318             push( @args, qq{"$node_bin"}, qq{"$server_bin"} );
319             }
320             else {
321 0           push( @args, $node_bin, $server_bin );
322             }
323              
324 0           push( @args, ( "--port", $port ) );
325 0 0         push( @args, "--cdp", $cdp_uri ) if $cdp_uri;
326 0 0         push( @args, $debug ) if $debug;
327              
328 0 0         $ENV{DEBUG} = 'pw:api' if $debug;
329 0           return _start_server_windows( $port, $timeout, $debug, $cleanup, @args )
330             if IS_WIN;
331 0   0       my $pid = fork // confess("Could not fork");
332 0 0         if ($pid) {
333 0 0         print "Waiting for playwright server on port $port to come up...\n"
334             if $debug;
335 0 0         Net::EmptyPort::wait_port( $port, $timeout )
336             or confess( "Server never came up after " . $timeout . "s!" );
337 0 0         print "done\n" if $debug;
338              
339 0           return $pid;
340             }
341              
342             # Orphan the process in the event that cleanup => 0
343 0 0         if ( !$cleanup ) {
344 0           print "Detaching child process...\n";
345 0           chdir File::Temp::tempdir( CLEANUP => 1 );
346 0           require POSIX;
347 0 0         die "Cannot detach playwright_server process for persistence"
348             if POSIX::setsid() < 0;
349 0           require Capture::Tiny;
350 0     0     capture_merged { exec(@args) };
  0            
351 0           die("Could not exec!");
352             }
353 0           exec(@args);
354             }
355              
356 0     0     sub _start_server_windows ( $port, $timeout, $debug, $cleanup, @args ) {
  0            
  0            
  0            
  0            
  0            
  0            
357 0           my $pid = qq/playwright-server:$port/;
358 0           my @cmdprefix = ( "start /MIN", qq{"$pid"} );
359              
360             # Test::UnusedVars hack
361 0           $cleanup = '';
362              
363 0           my $cmdstring = join( ' ', @cmdprefix, @args );
364 0 0         print "$cmdstring\n" if $debug;
365 0           system($cmdstring);
366 0           _wait_port( $port, $timeout, $debug );
367 0           return $pid;
368             }
369              
370 0     0     sub _kill_playwright_server_windows ($self) {
  0            
  0            
371 0           my $killer = qq[taskkill /FI "WINDOWTITLE eq $self->{pid}"];
372 0 0         print "$killer\n" if $self->{debug};
373 0           system($killer);
374 0           return 1;
375             }
376              
377             1;
378              
379             __END__