Răsfoiți Sursa

Fix #51: add host option to connect to remote hosts

George Baugh 3 ani în urmă
părinte
comite
1ab18b6cc0

+ 4 - 0
conf/Changes

@@ -1,5 +1,9 @@
 Revision history for Playwright
 
+1.291 2022-12-28 TEODESIAN
+    - Add 'port' mechanism to connect to remote instances of playwright_server
+    - Add systemd service files for running things in user mode.  See service/Readme.md
+
 1.251 2022-08-21 TEODESIAN
     - Fix some undef value warnings in odd situations when using the --port option.
 

+ 1 - 1
dist.ini

@@ -1,5 +1,5 @@
 name = Playwright
-version = 1.251
+version = 1.291
 author = George S. Baugh <george@troglodyne.net>
 license = MIT
 copyright_holder = Troglodyne LLC

+ 164 - 146
example.pl

@@ -5,152 +5,170 @@ use Data::Dumper;
 use Playwright;
 use Try::Tiny;
 
-my $handle = Playwright->new( debug => 1 );
-
-# Open a new chrome instance
-my $browser = $handle->launch( headless => 1, type => 'firefox' );
-my $process = $handle->server( browser => $browser, command => 'process' );
-print "Browser PID: ".$process->{pid}."\n";
-
-# Open a tab therein
-my $page = $browser->newPage({ videosPath => 'video', acceptDownloads => 1 });
-
-# Test the spec method
-print Dumper($page->spec(),$page);
-
-# Browser contexts don't exist until you open at least one page.
-# You'll need this to grab and set cookies.
-my ($context) = @{$browser->contexts()};
-
-# Load a URL in the tab
-my $res = $page->goto('http://google.com', { waitUntil => 'networkidle' });
-print Dumper($res->status(), $browser->version());
-
-# Put your hand in the jar
-my $cookies = $context->cookies();
-print Dumper($cookies);
-
-# Grab the main frame, in case this is a frameset
-my $frameset = $page->mainFrame();
-print Dumper($frameset->childFrames());
-
-# Run some JS
-my $fun = "
-    var input = arguments[0];
-    return {
-        width: document.documentElement.clientWidth,
-        height: document.documentElement.clientHeight,
-        deviceScaleFactor: window.devicePixelRatio,
-        arg: input
-    };";
-my $result = $page->evaluate($fun, 'zippy');
-print Dumper($result);
-
-# Read the console
-$page->on('console',"return [...arguments]");
-
-my $promise = $page->waitForEvent('console');
-#XXX this *can* race
-sleep 1;
-$page->evaluate("console.log('hug')");
-my $console_log = $handle->await( $promise );
-
-print "Logged to console: '".$console_log->text()."'\n";
-
-# Use a selector to find which input is visible and type into it
-# Ideally you'd use a better selector to solve this problem, but this is just showing off
-my $inputs = $page->selectMulti('input');
-
-foreach my $input (@$inputs) {
-    try {
-        # Pretty much a brute-force approach here, again use a better pseudo-selector instead like :visible
-        $input->fill('tickle', { timeout => 250 } );
-    } catch {
-        print "Element not visible, skipping...\n";
+{
+    my $handle = Playwright->new( debug => 1 );
+
+    # Open a new chrome instance
+    my $browser = $handle->launch( headless => 1, type => 'firefox' );
+    my $process = $handle->server( browser => $browser, command => 'process' );
+    print "Browser PID: ".$process->{pid}."\n";
+
+    # Open a tab therein
+    my $page = $browser->newPage({ videosPath => 'video', acceptDownloads => 1 });
+
+    # Test the spec method
+    print Dumper($page->spec(),$page);
+
+   # Browser contexts don't exist until you open at least one page.
+    # You'll need this to grab and set cookies.
+    my ($context) = @{$browser->contexts()};
+
+    # Load a URL in the tab
+    my $res = $page->goto('http://troglodyne.net', { waitUntil => 'networkidle' });
+    print Dumper($res->status(), $browser->version());
+
+    # Put your hand in the jar
+    my $cookies = $context->cookies();
+    print Dumper($cookies);
+
+    # Grab the main frame, in case this is a frameset
+    my $frameset = $page->mainFrame();
+    print Dumper($frameset->childFrames());
+
+    # Run some JS
+    my $fun = "
+        var input = arguments[0];
+        return {
+            width: document.documentElement.clientWidth,
+            height: document.documentElement.clientHeight,
+            deviceScaleFactor: window.devicePixelRatio,
+            arg: input
+        };";
+    my $result = $page->evaluate($fun, 'zippy');
+    print Dumper($result);
+
+    # Read the console
+    $page->on('console',"return [...arguments]");
+
+    my $promise = $page->waitForEvent('console');
+    #XXX this *can* race
+    sleep 1;
+    $page->evaluate("console.log('hug')");
+    my $console_log = $handle->await( $promise );
+
+    print "Logged to console: '".$console_log->text()."'\n";
+
+    # Use a selector to find which input is visible and type into it
+    # Ideally you'd use a better selector to solve this problem, but this is just showing off
+    my $inputs = $page->selectMulti('input');
+
+    foreach my $input (@$inputs) {
+        try {
+            # Pretty much a brute-force approach here, again use a better pseudo-selector instead like :visible
+            $input->fill('tickle', { timeout => 250 } );
+        } catch {
+            print "Element not visible, skipping...\n";
+        }
     }
+
+    # Said better selector
+    my $actual_input = $page->select('input[name=like]');
+    $actual_input->fill('whee');
+
+    # Ensure we can grab the parent (convenience)
+    print "Got Parent: ISA ".ref($actual_input->{parent})."\n";
+
+    # Take screen of said element
+    $actual_input->screenshot({ path => 'test.jpg' });
+
+    # Fiddle with HIDs
+    my $mouse = $page->mouse;
+    $mouse->move( 0, 0 );
+    my $keyboard = $page->keyboard();
+    $keyboard->type('F12');
+
+    # Start to do some more advanced actions with the page
+    use FindBin;
+    use Cwd qw{abs_path};
+    my $pg = abs_path("$FindBin::Bin/at/test.html");
+
+    # Handle dialogs on page start, and dialog after dialog
+    # NOTE -- the 'load' event won't fire until the dialog is dismissed in some browsers
+    $promise = $page->waitForEvent('dialog');
+    $page->goto("file://$pg", { waitUntil => 'networkidle' });
+
+    my $dlg = $handle->await($promise);
+    $promise = $page->waitForEvent('dialog');
+    $dlg->dismiss();
+    $dlg = $handle->await($promise);
+    $dlg->accept();
+
+    # Download stuff -- note this requries acceptDownloads = true in the page open
+    # NOTE -- the 'download' event fires unreliably, as not all browsers properly obey the 'download' property in hrefs.
+    # Chrome, for example would choke here on an intermediate dialog.
+    $promise = $page->waitForEvent('download');
+    $page->select('#d-lo')->click();
+
+    my $download = $handle->await( $promise );
+
+    print "Download suggested filename\n";
+    print $download->suggestedFilename()."\n";
+    $download->saveAs('test2.jpg');
+
+    # Fiddle with file inputs
+    my $choochoo = $page->waitForEvent('filechooser');
+    $page->select('#drphil')->click();
+    my $chooseu = $handle->await( $choochoo );
+    $chooseu->setFiles('test.jpg');
+
+    # Make sure we can do child selectors
+    my $parent = $page->select('body');
+    my $child = $parent->select('#drphil');
+    print ref($child)."\n";
+
+    # Test out pusht/popt/try_until
+
+    # Timeouts are in milliseconds
+    Playwright::pusht($page,5000);
+    my $checkpoint = time();
+    my $element = Playwright::try_until($page, 'select', 'bogus-bogus-nothere');
+
+    my $elapsed = time() - $checkpoint;
+    Playwright::popt($page);
+    print "Waited $elapsed seconds for timeout to drop\n";
+
+    $checkpoint = time();
+    $element = Playwright::try_until($page, 'select', 'bogus-bogus-nothere');
+    $elapsed = time() - $checkpoint;
+    print "Waited $elapsed seconds for timeout to drop\n";
+
+    # Try out the API testing extensions
+    print "HEAD http://troglodyne.net : \n";
+    my $fr = $page->request;
+    my $resp = $fr->fetch("http://troglodyne.net", { method => "HEAD" });
+    print Dumper($resp->headers());
+    print "200 OK\n" if $resp->status() == 200;
+
+    # Save a video now that we are done
+    my $bideo = $page->video;
+
+    # IT IS IMPORTANT TO CLOSE THE PAGE FIRST OR THIS WILL HANG!
+    $page->close();
+    my $vidpath = $bideo->saveAs('video/example.webm');
+}
+
+# Example of using persistent mode / remote hosts
+{
+    my $handle  = Playwright->new( debug => 1 );
+    my $handle2 = Playwright->new( debug => 1, host => 'localhost', port => $handle->{port} );
+
+    my $browser = $handle2->launch( headless => 1, type => 'firefox' );
+    my $process = $handle2->server( browser => $browser, command => 'process' );
+    print "Browser PID: ".$process->{pid}."\n";
+
 }
 
-# Said better selector
-my $actual_input = $page->select('input[name=q]');
-$actual_input->fill('whee');
-
-# Ensure we can grab the parent (convenience)
-print "Got Parent: ISA ".ref($actual_input->{parent})."\n";
-
-# Take screen of said element
-$actual_input->screenshot({ path => 'test.jpg' });
-
-# Fiddle with HIDs
-my $mouse = $page->mouse;
-$mouse->move( 0, 0 );
-my $keyboard = $page->keyboard();
-$keyboard->type('F12');
-
-# Start to do some more advanced actions with the page
-use FindBin;
-use Cwd qw{abs_path};
-my $pg = abs_path("$FindBin::Bin/at/test.html");
-
-# Handle dialogs on page start, and dialog after dialog
-# NOTE -- the 'load' event won't fire until the dialog is dismissed in some browsers
-$promise = $page->waitForEvent('dialog');
-$page->goto("file://$pg", { waitUntil => 'networkidle' });
-
-my $dlg = $handle->await($promise);
-$promise = $page->waitForEvent('dialog');
-$dlg->dismiss();
-$dlg = $handle->await($promise);
-$dlg->accept();
-
-# Download stuff -- note this requries acceptDownloads = true in the page open
-# NOTE -- the 'download' event fires unreliably, as not all browsers properly obey the 'download' property in hrefs.
-# Chrome, for example would choke here on an intermediate dialog.
-$promise = $page->waitForEvent('download');
-$page->select('#d-lo')->click();
-
-my $download = $handle->await( $promise );
-
-print "Download suggested filename\n";
-print $download->suggestedFilename()."\n";
-$download->saveAs('test2.jpg');
-
-# Fiddle with file inputs
-my $choochoo = $page->waitForEvent('filechooser');
-$page->select('#drphil')->click();
-my $chooseu = $handle->await( $choochoo );
-$chooseu->setFiles('test.jpg');
-
-# Make sure we can do child selectors
-my $parent = $page->select('body');
-my $child = $parent->select('#drphil');
-print ref($child)."\n";
-
-# Test out pusht/popt/try_until
-
-# Timeouts are in milliseconds
-Playwright::pusht($page,5000);
-my $checkpoint = time();
-my $element = Playwright::try_until($page, 'select', 'bogus-bogus-nothere');
-
-my $elapsed = time() - $checkpoint;
-Playwright::popt($page);
-print "Waited $elapsed seconds for timeout to drop\n";
-
-$checkpoint = time();
-$element = Playwright::try_until($page, 'select', 'bogus-bogus-nothere');
-$elapsed = time() - $checkpoint;
-print "Waited $elapsed seconds for timeout to drop\n";
-
-# Try out the API testing extensions
-print "HEAD http://google.com : \n";
-my $fr = $page->request;
-my $resp = $fr->fetch("http://google.com", { method => "HEAD" });
-print Dumper($resp->headers());
-print "200 OK\n" if $resp->status() == 200;
-
-# Save a video now that we are done
-my $bideo = $page->video;
-
-# IT IS IMPORTANT TO CLOSE THE PAGE FIRST OR THIS WILL HANG!
-$page->close();
-my $vidpath = $bideo->saveAs('video/example.webm');
+# Clean up, since we left survivors
+require './bin/reap_playwright_servers';
+Playwright::ServerReaper::main();
+0;

+ 15 - 7
lib/Playwright.pm

@@ -213,6 +213,13 @@ To save on memory, this is a good idea.  Pass the 'port' argument to the constru
 
 This will also set the cleanup flag to false, so be sure you run `reap_playwright_servers` when you are sure that all testing on this server is done.
 
+=head2 Running against remote playwright servers
+
+Pass the 'host' along with the 'port' argument to the constructor in order to use an instance of playwright_server running on another host.
+This will naturally set the cleanup flag to false; it is the server operator's responsibility to reap the server when complete.
+
+A systemd service file, and Makefile are provided in the service/ folder of this module's git repository which will install playwright_server as a user-mode service on the PORT variable.
+
 =head2 Taking videos, Making Downloads
 
 We spawn browsers via BrowserType.launchServer() and then connect to them over websocket.
@@ -442,14 +449,15 @@ sub new ( $class, %options ) {
     my $self = bless(
         {
             ua      => $options{ua} // LWP::UserAgent->new(),
+            host    => $options{host} // 'localhost',
             port    => $port,
             debug   => $options{debug},
-            cleanup => ( $options{cleanup} || !$options{port} ) // 1,
-            pid     => _start_server( $port, $timeout, $options{debug}, $options{cleanup} // 1 ),
+            cleanup => ( $options{cleanup} || !$options{port} || !$options{host} ) // 1,
+            pid     => $options{host} ? "REUSE" : _start_server( $port, $timeout, $options{debug}, $options{cleanup} // 1 ),
             parent  => $$ // 'bogus', # Oh lawds, this can be undef sometimes
             timeout => $timeout,
         },
-        $class
+        $class,
     );
 
     $self->_check_and_build_spec();
@@ -462,7 +470,7 @@ sub _check_and_build_spec ($self) {
     return $spec if ref $spec eq 'HASH';
 
     $spec = Playwright::Util::request(
-        'GET', 'spec', $self->{port}, $self->{ua},
+        'GET', 'spec', $self->{host}, $self->{port}, $self->{ua},
     );
 
     confess("Could not retrieve Playwright specification.  Check that your playwright installation is correct and complete.") unless ref $spec eq 'HASH';
@@ -490,7 +498,7 @@ sub launch ( $self, %args ) {
     delete $args{command};
 
     my $msg = Playwright::Util::request(
-        'POST', 'session', $self->{port}, $self->{ua},
+        'POST', 'session', $self->{host}, $self->{port}, $self->{ua},
         type => delete $args{type},
         args => [ \%args ]
     );
@@ -523,7 +531,7 @@ BrowserServer methods (at the time of writing) take no arguments, so they are no
 
 sub server ( $self, %args ) {
     return Playwright::Util::request(
-        'POST', 'server', $self->{port}, $self->{ua},
+        'POST', 'server', $self->{host}, $self->{port}, $self->{ua},
         object  => $args{browser}{guid},
         command => $args{command},
     );
@@ -628,7 +636,7 @@ sub quit ($self) {
     $self->{killed} = 1;
     print "Attempting to terminate server process...\n" if $self->{debug};
 
-    Playwright::Util::request( 'GET', 'shutdown', $self->{port}, $self->{ua} );
+    Playwright::Util::request( 'GET', 'shutdown', $self->{host}, $self->{port}, $self->{ua} );
 
     # 0 is always WCONTINUED, 1 is always WNOHANG, and POSIX is an expensive import
     # When 0 is returned, the process is still active, so it needs more persuasion

+ 2 - 1
lib/Playwright/Base.pm

@@ -49,6 +49,7 @@ sub new ( $class, %options ) {
             guid   => $options{id},
             ua     => $options{handle}{ua},
             port   => $options{handle}{port},
+            host   => $options{handle}{host},
             parent => $options{parent},
         },
         $class
@@ -112,7 +113,7 @@ sub _api_request ( $self, %args ) {
 }
 
 sub _do ( $self, %args ) {
-    return Playwright::Util::request( 'POST', 'command', $self->{port},
+    return Playwright::Util::request( 'POST', 'command', $self->{host}, $self->{port},
         $self->{ua}, %args );
 }
 

+ 3 - 3
lib/Playwright/Util.pm

@@ -17,14 +17,14 @@ use POSIX();
 no warnings 'experimental';
 use feature qw{signatures};
 
-=head2 request(STRING method, STRING url, INTEGER port, LWP::UserAgent ua, HASH args) = HASH
+=head2 request(STRING method, STRING url, STRING host, INTEGER port, LWP::UserAgent ua, HASH args) = HASH
 
 De-duplicates request logic in the Playwright Modules.
 
 =cut
 
-sub request ( $method, $url, $port, $ua, %args ) {
-    my $fullurl = "http://localhost:$port/$url";
+sub request ( $method,$url, $host, $port, $ua, %args ) {
+    my $fullurl = "http://$host:$port/$url";
 
     # Handle passing Playwright elements as arguments
     if (ref $args{args} eq 'ARRAY') {

+ 1 - 1
package.json

@@ -6,7 +6,7 @@
   "private": true,
   "dependencies": {
     "express": "^4.17",
-    "playwright": "^1.25.0",
+    "playwright": "^1.29.1",
     "uuid": "^8.3"
   }
 }

+ 37 - 0
service/Makefile

@@ -0,0 +1,37 @@
+PATH := "$(NVM_BIN):$(PATH)"
+UID := $(shell id -u)
+NVM_BINARY := $(shell which node)
+
+.PHONY: install-deps
+install-deps:
+	which npm
+	which npx
+	chmod +x ~/.nvm/nvm.sh
+	cd .. && npm i
+	cd .. && npm i playwright@latest
+	cd .. && npx playwright install-deps
+	cd .. && ./generate_api_json.sh
+	cd .. && ./generate_perl_modules.pl
+
+.PHONY: install-service
+install-service:
+	test $(PORT)
+	test $(NVM_BINARY)
+	make install-deps
+	[ ! -f /etc/redhat-release ] || make enable-systemd-user-mode
+	mkdir -p ~/.config/systemd/user
+	cp playwright.unit ~/.config/systemd/user/playwright.service
+	sed -i 's#__REPLACEME__#$(shell pwd)#g' ~/.config/systemd/user/playwright.service
+	sed -i 's#__PORT__#$(PORT)#g' ~/.config/systemd/user/playwright.service
+	sed -i 's#!/usr/bin/node#!$(NVM_BINARY)#g' ../bin/playwright_server
+	systemctl --user daemon-reload
+	systemctl --user enable playwright
+	systemctl --user start playwright
+	sudo loginctl enable-linger $(USER)
+
+.PHONY: enable-systemd-user-mode
+enable-systemd-user-mode:
+	sudo cp systemd/centos-user-mode.unit /etc/systemd/system/user@$(UID).service
+	sudo systemctl daemon-reload
+	sudo systemctl enable user@$(UID).service
+	sudo systemctl start user@$(UID).service

+ 21 - 0
service/Readme.md

@@ -0,0 +1,21 @@
+# SystemD service files
+
+These are tested on ubuntu and centos, but should generally work on similar distros.
+Contributions welcome for other distros and init systems.
+
+## Setting up
+
+This assumes you have already `nvm install` and `nvm use node` on your desired node version.
+The node binary used at the time you run the makefile will be hardcoded into `playwright_server`.
+
+Run `PORT=6969 make install-service` and things should "just work (TM)".
+Replace port as appropriate.
+
+Manage service with `systemctl --user $VERB playwright`
+where $VERB is reload, restart, stop et cetera.
+
+## TODO
+
+Make playwright\_server reload on HUP
+
+Make playwright\_server have superdaemon functionality (see issue #52)

+ 33 - 0
service/centos-user-mode.unit

@@ -0,0 +1,33 @@
+# See https://serverfault.com/questions/936985/cannot-use-systemctl-user-due-to-failed-to-get-d-bus-connection-permission
+# for why we have to do this kind of thing on CentOS/AmazonLinux
+
+[Unit]
+Description=User Manager for UID %i
+After=systemd-user-sessions.service
+# These are present in the RHEL8 version of this file except that the unit is Requires, not Wants.
+# It's listed as Wants here so that if this file is used in a RHEL7 settings, it will not fail.
+# If a user upgrades from RHEL7 to RHEL8, this unit file will continue to work until it's
+# deleted the next time they upgrade Tableau Server itself.
+After=user-runtime-dir@%i.service
+Wants=user-runtime-dir@%i.service
+
+[Service]
+LimitNOFILE=infinity
+LimitNPROC=infinity
+User=%i
+PAMName=systemd-user
+Type=notify
+# PermissionsStartOnly is deprecated and will be removed in future versions of systemd
+# This is required for all systemd versions prior to version 231
+PermissionsStartOnly=true
+ExecStartPre=/bin/loginctl enable-linger %i
+ExecStart=-/lib/systemd/systemd --user
+Slice=user-%i.slice
+KillMode=mixed
+Delegate=yes
+TasksMax=infinity
+Restart=always
+RestartSec=15
+
+[Install]
+WantedBy=default.target

+ 10 - 0
service/playwright.unit

@@ -0,0 +1,10 @@
+[Unit]
+Description=playwright
+
+[Install]
+WantedBy=default.target
+
+[Service]
+ExecStart=__REPLACEME__/../bin/playwright_server -p __PORT__
+ExecReload=/usr/bin/env kill -s HUP $MAINPID
+WorkingDirectory=__REPLACEME__/

+ 2 - 2
t/Playwright-Util.t

@@ -15,11 +15,11 @@ local *BogusResponse::decoded_content = sub {
 };
 use warnings;
 
-like( dies { Playwright::Util::request('tickle','chase',666, LWP::UserAgent->new(), a => 'b' ) }, qr/waa/i, "Bad response from server = BOOM");
+like( dies { Playwright::Util::request('tickle','chase', 'localhost', 666, LWP::UserAgent->new(), a => 'b' ) }, qr/waa/i, "Bad response from server = BOOM");
 
 $json = '{ "error":false, "message": { "_type":"Bogus", "_guid":"abc123" } }';
 
-is(Playwright::Util::request('tickle','chase',666, LWP::UserAgent->new(), a => 'b' ), { _type => 'Bogus', _guid => 'abc123' }, "Good response from server decoded and returned");
+is(Playwright::Util::request('tickle','chase', 'localhost', 666, LWP::UserAgent->new(), a => 'b' ), { _type => 'Bogus', _guid => 'abc123' }, "Good response from server decoded and returned");
 
 #Not testing async/await, mocking forks is bogus
 

+ 2 - 0
t/Playwright.t

@@ -83,6 +83,7 @@ subtest "new" => sub {
         parent => $$,
         pid    => 666,
         port   => 420,
+        host   => 'localhost',
         timeout => 5,
         cleanup => 1,
     }, 'Playwright');
@@ -95,6 +96,7 @@ subtest "new" => sub {
         parent => $$,
         pid    => 666,
         port   => 420,
+        host   => 'localhost',
         timeout => 30,
         cleanup => 1,
     }, 'Playwright');