From 6d982db6b923b64a630a9ee5fb381998fc52780b Mon Sep 17 00:00:00 2001 From: Yury Semikhatsky Date: Tue, 2 Mar 2021 18:13:27 -0800 Subject: [PATCH] fix: implement BrowserType.connect (#314) --- playwright/pom.xml | 9 +- .../com/microsoft/playwright/BrowserType.java | 35 ++++ .../playwright/impl/BrowserImpl.java | 1 + .../playwright/impl/BrowserTypeImpl.java | 37 ++++ .../microsoft/playwright/impl/Connection.java | 7 +- .../playwright/impl/DownloadImpl.java | 21 +++ .../playwright/impl/PipeTransport.java | 167 ++++++++++++++++++ .../playwright/impl/PlaywrightImpl.java | 9 +- .../playwright/impl/RemoteBrowser.java | 33 ++++ .../playwright/impl/SelectorsImpl.java | 22 +-- .../playwright/impl/SharedSelectors.java | 77 ++++++++ .../microsoft/playwright/impl/Transport.java | 150 +--------------- .../com/microsoft/playwright/impl/Utils.java | 23 ++- .../playwright/impl/WebSocketTransport.java | 100 +++++++++++ pom.xml | 1 - scripts/CLI_VERSION | 2 +- 16 files changed, 516 insertions(+), 178 deletions(-) create mode 100644 playwright/src/main/java/com/microsoft/playwright/impl/PipeTransport.java create mode 100644 playwright/src/main/java/com/microsoft/playwright/impl/RemoteBrowser.java create mode 100644 playwright/src/main/java/com/microsoft/playwright/impl/SharedSelectors.java create mode 100644 playwright/src/main/java/com/microsoft/playwright/impl/WebSocketTransport.java diff --git a/playwright/pom.xml b/playwright/pom.xml index 90526687..ec7ba219 100644 --- a/playwright/pom.xml +++ b/playwright/pom.xml @@ -61,6 +61,10 @@ com.google.code.gson gson + + org.java-websocket + Java-WebSocket + org.junit.jupiter junit-jupiter-engine @@ -73,10 +77,5 @@ com.microsoft.playwright driver-bundle - - org.java-websocket - Java-WebSocket - test - diff --git a/playwright/src/main/java/com/microsoft/playwright/BrowserType.java b/playwright/src/main/java/com/microsoft/playwright/BrowserType.java index 9359ba46..f9f6af9d 100644 --- a/playwright/src/main/java/com/microsoft/playwright/BrowserType.java +++ b/playwright/src/main/java/com/microsoft/playwright/BrowserType.java @@ -41,6 +41,27 @@ import java.util.*; * } */ public interface BrowserType { + class ConnectOptions { + /** + * Slows down Playwright operations by the specified amount of milliseconds. Useful so that you can see what is going on. + * Defaults to 0. + */ + public Double slowMo; + /** + * Maximum time in milliseconds to wait for the connection to be established. Defaults to {@code 30000} (30 seconds). Pass {@code 0} to + * disable timeout. + */ + public Double timeout; + + public ConnectOptions withSlowMo(double slowMo) { + this.slowMo = slowMo; + return this; + } + public ConnectOptions withTimeout(double timeout) { + this.timeout = timeout; + return this; + } + } class LaunchOptions { /** * Additional arguments to pass to the browser instance. The list of Chromium flags can be found contexts = new HashSet<>(); private final ListenerCollection listeners = new ListenerCollection<>(); + public boolean isRemote; private boolean isConnected = true; enum EventType { diff --git a/playwright/src/main/java/com/microsoft/playwright/impl/BrowserTypeImpl.java b/playwright/src/main/java/com/microsoft/playwright/impl/BrowserTypeImpl.java index 75fd236d..b32528ec 100644 --- a/playwright/src/main/java/com/microsoft/playwright/impl/BrowserTypeImpl.java +++ b/playwright/src/main/java/com/microsoft/playwright/impl/BrowserTypeImpl.java @@ -18,10 +18,15 @@ package com.microsoft.playwright.impl; import com.google.gson.JsonElement; import com.google.gson.JsonObject; +import com.microsoft.playwright.Browser; import com.microsoft.playwright.BrowserType; import com.microsoft.playwright.PlaywrightException; +import java.io.IOException; +import java.net.URI; +import java.net.URISyntaxException; import java.nio.file.Path; +import java.time.Duration; import static com.microsoft.playwright.impl.Serialization.gson; @@ -44,6 +49,38 @@ class BrowserTypeImpl extends ChannelOwner implements BrowserType { return connection.getExistingObject(result.getAsJsonObject().getAsJsonObject("browser").get("guid").getAsString()); } + @Override + public Browser connect(String wsEndpoint, ConnectOptions options) { + return withLogging("BrowserType.connect", () -> connectImpl(wsEndpoint, options)); + } + + private Browser connectImpl(String wsEndpoint, ConnectOptions options) { + try { + Duration timeout = Duration.ofDays(1); + if (options != null && options.timeout != null) { + timeout = Duration.ofMillis(Math.round(options.timeout)); + } + Connection connection = new Connection(new WebSocketTransport(new URI(wsEndpoint), timeout)); + RemoteBrowser remoteBrowser = (RemoteBrowser) connection.waitForObjectWithKnownName("remoteBrowser"); + PlaywrightImpl playwright = this.connection.getExistingObject("Playwright"); + SelectorsImpl selectors = remoteBrowser.selectors(); + playwright.sharedSelectors.addChannel(selectors); + BrowserImpl browser = remoteBrowser.browser(); + browser.isRemote = true; + browser.onDisconnected(b -> { + playwright.sharedSelectors.removeChannel(selectors); + try { + connection.close(); + } catch (IOException e) { + e.printStackTrace(); + } + }); + return browser; + } catch (URISyntaxException e) { + throw new PlaywrightException("Failed to connect", e); + } + } + public String executablePath() { return initializer.get("executablePath").getAsString(); } diff --git a/playwright/src/main/java/com/microsoft/playwright/impl/Connection.java b/playwright/src/main/java/com/microsoft/playwright/impl/Connection.java index fe8ae9c1..0461b9d4 100644 --- a/playwright/src/main/java/com/microsoft/playwright/impl/Connection.java +++ b/playwright/src/main/java/com/microsoft/playwright/impl/Connection.java @@ -64,8 +64,8 @@ public class Connection { } } - public Connection(InputStream in, OutputStream out) { - transport = new Transport(in, out); + Connection(Transport transport) { + this.transport = transport; root = new Root(this); } @@ -232,6 +232,9 @@ public class Connection { case "Request": result = new RequestImpl(parent, type, guid, initializer); break; + case "RemoteBrowser": + result = new RemoteBrowser(parent, type, guid, initializer); + break; case "Response": result = new ResponseImpl(parent, type, guid, initializer); break; diff --git a/playwright/src/main/java/com/microsoft/playwright/impl/DownloadImpl.java b/playwright/src/main/java/com/microsoft/playwright/impl/DownloadImpl.java index 8bb2b419..abe524c8 100644 --- a/playwright/src/main/java/com/microsoft/playwright/impl/DownloadImpl.java +++ b/playwright/src/main/java/com/microsoft/playwright/impl/DownloadImpl.java @@ -16,16 +16,27 @@ package com.microsoft.playwright.impl; +import com.google.gson.JsonElement; import com.google.gson.JsonObject; +import com.microsoft.playwright.BrowserContext; import com.microsoft.playwright.Download; +import com.microsoft.playwright.PlaywrightException; +import java.io.FileNotFoundException; +import java.io.FileOutputStream; +import java.io.IOException; import java.io.InputStream; import java.nio.file.FileSystems; import java.nio.file.Path; +import static com.microsoft.playwright.impl.Utils.writeToFile; + public class DownloadImpl extends ChannelOwner implements Download { + private final BrowserImpl browser; + public DownloadImpl(ChannelOwner parent, String type, String guid, JsonObject initializer) { super(parent, type, guid, initializer); + browser = ((BrowserContextImpl) parent).browser(); } @Override @@ -71,6 +82,9 @@ public class DownloadImpl extends ChannelOwner implements Download { @Override public Path path() { return withLogging("Download.path", () -> { + if (browser != null && browser.isRemote) { + throw new PlaywrightException("Path is not available when using browserType.connect(). Use download.saveAs() to save a local copy."); + } JsonObject json = sendMessage("path").getAsJsonObject(); return FileSystems.getDefault().getPath(json.get("value").getAsString()); }); @@ -79,6 +93,13 @@ public class DownloadImpl extends ChannelOwner implements Download { @Override public void saveAs(Path path) { withLogging("Download.saveAs", () -> { + if (browser != null && browser.isRemote) { + JsonObject jsonObject = sendMessage("saveAsStream").getAsJsonObject(); + Stream stream = connection.getExistingObject(jsonObject.getAsJsonObject("stream").get("guid").getAsString()); + writeToFile(stream.stream(), path); + return; + } + JsonObject params = new JsonObject(); params.addProperty("path", path.toString()); sendMessage("saveAs", params); diff --git a/playwright/src/main/java/com/microsoft/playwright/impl/PipeTransport.java b/playwright/src/main/java/com/microsoft/playwright/impl/PipeTransport.java new file mode 100644 index 00000000..4fe86f65 --- /dev/null +++ b/playwright/src/main/java/com/microsoft/playwright/impl/PipeTransport.java @@ -0,0 +1,167 @@ +/* + * Copyright (c) Microsoft Corporation. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.microsoft.playwright.impl; + +import com.microsoft.playwright.PlaywrightException; + +import java.io.*; +import java.nio.charset.StandardCharsets; +import java.time.Duration; +import java.util.concurrent.ArrayBlockingQueue; +import java.util.concurrent.BlockingQueue; +import java.util.concurrent.TimeUnit; + +public class PipeTransport implements Transport { + private final BlockingQueue incoming = new ArrayBlockingQueue<>(1000); + private final BlockingQueue outgoing= new ArrayBlockingQueue<>(1000); + + private final ReaderThread readerThread; + private final WriterThread writerThread; + + private boolean isClosed; + + PipeTransport(InputStream input, OutputStream output) { + DataInputStream in = new DataInputStream(new BufferedInputStream(input)); + readerThread = new ReaderThread(in, incoming); + readerThread.start(); + writerThread = new WriterThread(output, outgoing); + writerThread.start(); + } + + @Override + public void send(String message) { + if (isClosed) { + throw new PlaywrightException("Playwright connection closed"); + } + try { + outgoing.put(message); + } catch (InterruptedException e) { + throw new PlaywrightException("Failed to send message", e); + } + } + + @Override + public String poll(Duration timeout) { + if (isClosed) { + throw new PlaywrightException("Playwright connection closed"); + } + try { + return incoming.poll(timeout.toMillis(), TimeUnit.MILLISECONDS); + } catch (InterruptedException e) { + throw new PlaywrightException("Failed to read message", e); + } + } + + @Override + public void close() throws IOException { + if (isClosed) { + return; + } + isClosed = true; + // We interrupt only the outgoing pipe and keep reader thread running as + // otherwise child process may block on writing to its stdout and never + // exit (observed on Windows). + readerThread.isClosing = true; + writerThread.out.close(); + writerThread.interrupt(); + } +} + +class ReaderThread extends Thread { + private final DataInputStream in; + private final BlockingQueue queue; + volatile boolean isClosing; + + private static int readIntLE(DataInputStream in) throws IOException { + int ch1 = in.read(); + int ch2 = in.read(); + int ch3 = in.read(); + int ch4 = in.read(); + if ((ch1 | ch2 | ch3 | ch4) < 0) { + throw new EOFException(); + } else { + return (ch4 << 24) + (ch3 << 16) + (ch2 << 8) + (ch1 << 0); + } + } + + ReaderThread(DataInputStream in, BlockingQueue queue) { + this.in = in; + this.queue = queue; + } + + @Override + public void run() { + while (!isInterrupted()) { + try { + queue.put(readMessage()); + } catch (IOException e) { + if (!isInterrupted() && !isClosing) { + e.printStackTrace(); + } + break; + } catch (InterruptedException e) { + break; + } + } + } + + private String readMessage() throws IOException { + int len = readIntLE(in); + byte[] raw = new byte[len]; + in.readFully(raw, 0, len); + return new String(raw, StandardCharsets.UTF_8); + } +} + +class WriterThread extends Thread { + final OutputStream out; + private final BlockingQueue queue; + + private static void writeIntLE(OutputStream out, int v) throws IOException { + out.write(v >>> 0 & 255); + out.write(v >>> 8 & 255); + out.write(v >>> 16 & 255); + out.write(v >>> 24 & 255); + } + + WriterThread(OutputStream out, BlockingQueue queue) { + this.out = out; + this.queue = queue; + } + + @Override + public void run() { + while (!isInterrupted()) { + try { + if (queue.isEmpty()) + out.flush(); + sendMessage(queue.take()); + } catch (IOException e) { + if (!isInterrupted()) + e.printStackTrace(); + break; + } catch (InterruptedException e) { + break; + } + } + } + + private void sendMessage(String message) throws IOException { + byte[] bytes = message.getBytes(StandardCharsets.UTF_8); + writeIntLE(out, bytes.length); + out.write(bytes); + } +} diff --git a/playwright/src/main/java/com/microsoft/playwright/impl/PlaywrightImpl.java b/playwright/src/main/java/com/microsoft/playwright/impl/PlaywrightImpl.java index ae45571e..30552d55 100644 --- a/playwright/src/main/java/com/microsoft/playwright/impl/PlaywrightImpl.java +++ b/playwright/src/main/java/com/microsoft/playwright/impl/PlaywrightImpl.java @@ -35,7 +35,7 @@ public class PlaywrightImpl extends ChannelOwner implements Playwright { pb.redirectError(ProcessBuilder.Redirect.INHERIT); // pb.environment().put("DEBUG", "pw:pro*"); Process p = pb.start(); - Connection connection = new Connection(p.getInputStream(), p.getOutputStream()); + Connection connection = new Connection(new PipeTransport(p.getInputStream(), p.getOutputStream())); PlaywrightImpl result = (PlaywrightImpl) connection.waitForObjectWithKnownName("Playwright"); result.driverProcess = p; return result; @@ -47,14 +47,15 @@ public class PlaywrightImpl extends ChannelOwner implements Playwright { private final BrowserTypeImpl chromium; private final BrowserTypeImpl firefox; private final BrowserTypeImpl webkit; - private final Selectors selectors; + final SharedSelectors sharedSelectors = new SharedSelectors();; PlaywrightImpl(ChannelOwner parent, String type, String guid, JsonObject initializer) { super(parent, type, guid, initializer); chromium = parent.connection.getExistingObject(initializer.getAsJsonObject("chromium").get("guid").getAsString()); firefox = parent.connection.getExistingObject(initializer.getAsJsonObject("firefox").get("guid").getAsString()); webkit = parent.connection.getExistingObject(initializer.getAsJsonObject("webkit").get("guid").getAsString()); - selectors = parent.connection.getExistingObject(initializer.getAsJsonObject("selectors").get("guid").getAsString()); + SelectorsImpl channel = parent.connection.getExistingObject(initializer.getAsJsonObject("selectors").get("guid").getAsString()); + sharedSelectors.addChannel(channel); } @Override @@ -74,7 +75,7 @@ public class PlaywrightImpl extends ChannelOwner implements Playwright { @Override public Selectors selectors() { - return selectors; + return sharedSelectors; } @Override diff --git a/playwright/src/main/java/com/microsoft/playwright/impl/RemoteBrowser.java b/playwright/src/main/java/com/microsoft/playwright/impl/RemoteBrowser.java new file mode 100644 index 00000000..0dc5011d --- /dev/null +++ b/playwright/src/main/java/com/microsoft/playwright/impl/RemoteBrowser.java @@ -0,0 +1,33 @@ +/* + * Copyright (c) Microsoft Corporation. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.microsoft.playwright.impl; + +import com.google.gson.JsonObject; + +public class RemoteBrowser extends ChannelOwner { + RemoteBrowser(ChannelOwner parent, String type, String guid, JsonObject initializer) { + super(parent, type, guid, initializer); + } + + BrowserImpl browser() { + return connection.getExistingObject(initializer.getAsJsonObject("browser").get("guid").getAsString()); + } + + SelectorsImpl selectors() { + return connection.getExistingObject(initializer.getAsJsonObject("selectors").get("guid").getAsString()); + } +} diff --git a/playwright/src/main/java/com/microsoft/playwright/impl/SelectorsImpl.java b/playwright/src/main/java/com/microsoft/playwright/impl/SelectorsImpl.java index 531cf695..e1e4a063 100644 --- a/playwright/src/main/java/com/microsoft/playwright/impl/SelectorsImpl.java +++ b/playwright/src/main/java/com/microsoft/playwright/impl/SelectorsImpl.java @@ -27,34 +27,18 @@ import java.nio.file.Path; import static com.microsoft.playwright.impl.Serialization.gson; import static java.nio.charset.StandardCharsets.UTF_8; -class SelectorsImpl extends ChannelOwner implements Selectors { +class SelectorsImpl extends ChannelOwner { SelectorsImpl(ChannelOwner parent, String type, String guid, JsonObject initializer) { super(parent, type, guid, initializer); } - @Override - public void register(String name, String script, RegisterOptions options) { - withLogging("Selectors.register", () -> registerImpl(name, script, options)); - } - - private void registerImpl(String name, String script, RegisterOptions options) { + void registerImpl(String name, String script, Selectors.RegisterOptions options) { if (options == null) { - options = new RegisterOptions(); + options = new Selectors.RegisterOptions(); } JsonObject params = gson().toJsonTree(options).getAsJsonObject(); params.addProperty("name", name); params.addProperty("source", script); sendMessage("register", params); } - - @Override - public void register(String name, Path path, RegisterOptions options) { - byte[] buffer; - try { - buffer = Files.readAllBytes(path); - } catch (IOException e) { - throw new PlaywrightException("Failed to read selector from file: " + path, e); - } - register(name, new String(buffer, UTF_8), options); - } } diff --git a/playwright/src/main/java/com/microsoft/playwright/impl/SharedSelectors.java b/playwright/src/main/java/com/microsoft/playwright/impl/SharedSelectors.java new file mode 100644 index 00000000..0da01512 --- /dev/null +++ b/playwright/src/main/java/com/microsoft/playwright/impl/SharedSelectors.java @@ -0,0 +1,77 @@ +/* + * Copyright (c) Microsoft Corporation. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.microsoft.playwright.impl; + +import com.microsoft.playwright.PlaywrightException; +import com.microsoft.playwright.Selectors; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.List; + +import static java.nio.charset.StandardCharsets.UTF_8; + +public class SharedSelectors extends LoggingSupport implements Selectors { + private final List channels = new ArrayList<>(); + private final List registrations = new ArrayList<>(); + + private static class Registration { + final String name; + final String script; + final RegisterOptions options; + + Registration(String name, String script, RegisterOptions options) { + this.name = name; + this.script = script; + this.options = options; + } + } + + @Override + public void register(String name, String script, RegisterOptions options) { + withLogging("Selectors.register", () -> registerImpl(name, script, options)); + } + + @Override + public void register(String name, Path path, RegisterOptions options) { + withLogging("Selectors.register", () -> { + byte[] buffer; + try { + buffer = Files.readAllBytes(path); + } catch (IOException e) { + throw new PlaywrightException("Failed to read selector from file: " + path, e); + } + registerImpl(name, new String(buffer, UTF_8), options); + }); + } + + void addChannel(SelectorsImpl channel) { + registrations.forEach(r -> channel.registerImpl(r.name, r.script, r.options)); + channels.add(channel); + } + + void removeChannel(SelectorsImpl channel) { + channels.remove(channel); + } + + private void registerImpl(String name, String script, RegisterOptions options) { + channels.forEach(impl -> impl.registerImpl(name, script, options)); + registrations.add(new Registration(name, script, options)); + } +} diff --git a/playwright/src/main/java/com/microsoft/playwright/impl/Transport.java b/playwright/src/main/java/com/microsoft/playwright/impl/Transport.java index af6959eb..9bfb0768 100644 --- a/playwright/src/main/java/com/microsoft/playwright/impl/Transport.java +++ b/playwright/src/main/java/com/microsoft/playwright/impl/Transport.java @@ -13,152 +13,14 @@ * See the License for the specific language governing permissions and * limitations under the License. */ + package com.microsoft.playwright.impl; -import com.microsoft.playwright.PlaywrightException; - -import java.io.*; -import java.nio.charset.StandardCharsets; +import java.io.IOException; import java.time.Duration; -import java.util.concurrent.ArrayBlockingQueue; -import java.util.concurrent.BlockingQueue; -import java.util.concurrent.TimeUnit; -public class Transport { - private final BlockingQueue incoming = new ArrayBlockingQueue<>(1000); - private final BlockingQueue outgoing= new ArrayBlockingQueue<>(1000); - - private final ReaderThread readerThread; - private final WriterThread writerThread; - - private boolean isClosed; - - Transport(InputStream input, OutputStream output) { - DataInputStream in = new DataInputStream(new BufferedInputStream(input)); - readerThread = new ReaderThread(in, incoming); - readerThread.start(); - writerThread = new WriterThread(output, outgoing); - writerThread.start(); - } - - public void send(String message) { - if (isClosed) { - throw new PlaywrightException("Playwright connection closed"); - } - try { - outgoing.put(message); - } catch (InterruptedException e) { - throw new PlaywrightException("Failed to send message", e); - } - } - - public String poll(Duration timeout) { - if (isClosed) { - throw new PlaywrightException("Playwright connection closed"); - } - try { - return incoming.poll(timeout.toMillis(), TimeUnit.MILLISECONDS); - } catch (InterruptedException e) { - throw new PlaywrightException("Failed to read message", e); - } - } - - void close() throws IOException { - if (isClosed) { - return; - } - isClosed = true; - // We interrupt only the outgoing pipe and keep reader thread running as - // otherwise child process may block on writing to its stdout and never - // exit (observed on Windows). - readerThread.isClosing = true; - writerThread.out.close(); - writerThread.interrupt(); - } -} - -class ReaderThread extends Thread { - private final DataInputStream in; - private final BlockingQueue queue; - volatile boolean isClosing; - - private static int readIntLE(DataInputStream in) throws IOException { - int ch1 = in.read(); - int ch2 = in.read(); - int ch3 = in.read(); - int ch4 = in.read(); - if ((ch1 | ch2 | ch3 | ch4) < 0) { - throw new EOFException(); - } else { - return (ch4 << 24) + (ch3 << 16) + (ch2 << 8) + (ch1 << 0); - } - } - - ReaderThread(DataInputStream in, BlockingQueue queue) { - this.in = in; - this.queue = queue; - } - - @Override - public void run() { - while (!isInterrupted()) { - try { - queue.put(readMessage()); - } catch (IOException e) { - if (!isInterrupted() && !isClosing) { - e.printStackTrace(); - } - break; - } catch (InterruptedException e) { - break; - } - } - } - - private String readMessage() throws IOException { - int len = readIntLE(in); - byte[] raw = new byte[len]; - in.readFully(raw, 0, len); - return new String(raw, StandardCharsets.UTF_8); - } -} - -class WriterThread extends Thread { - final OutputStream out; - private final BlockingQueue queue; - - private static void writeIntLE(OutputStream out, int v) throws IOException { - out.write(v >>> 0 & 255); - out.write(v >>> 8 & 255); - out.write(v >>> 16 & 255); - out.write(v >>> 24 & 255); - } - - WriterThread(OutputStream out, BlockingQueue queue) { - this.out = out; - this.queue = queue; - } - - @Override - public void run() { - while (!isInterrupted()) { - try { - if (queue.isEmpty()) - out.flush(); - sendMessage(queue.take()); - } catch (IOException e) { - if (!isInterrupted()) - e.printStackTrace(); - break; - } catch (InterruptedException e) { - break; - } - } - } - - private void sendMessage(String message) throws IOException { - byte[] bytes = message.getBytes(StandardCharsets.UTF_8); - writeIntLE(out, bytes.length); - out.write(bytes); - } +public interface Transport { + void send(String message); + String poll(Duration timeout); + void close() throws IOException; } diff --git a/playwright/src/main/java/com/microsoft/playwright/impl/Utils.java b/playwright/src/main/java/com/microsoft/playwright/impl/Utils.java index 6d673a70..f620b2ca 100644 --- a/playwright/src/main/java/com/microsoft/playwright/impl/Utils.java +++ b/playwright/src/main/java/com/microsoft/playwright/impl/Utils.java @@ -21,8 +21,10 @@ import com.microsoft.playwright.FileChooser; import com.microsoft.playwright.PlaywrightException; import com.microsoft.playwright.options.FilePayload; +import java.io.FileNotFoundException; import java.io.FileOutputStream; import java.io.IOException; +import java.io.InputStream; import java.nio.file.Files; import java.nio.file.Path; import java.util.*; @@ -126,8 +128,8 @@ class Utils { return payloads.toArray(new FilePayload[0]); } - static void writeToFile(byte[] buffer, Path path) { - Path dir = path.getParent(); + static void mkParentDirs(Path file) { + Path dir = file.getParent(); if (dir != null) { if (!Files.exists(dir)) { try { @@ -137,6 +139,10 @@ class Utils { } } } + } + + static void writeToFile(byte[] buffer, Path path) { + mkParentDirs(path); try (FileOutputStream out = new FileOutputStream(path.toFile())) { out.write(buffer); } catch (IOException e) { @@ -144,6 +150,19 @@ class Utils { } } + static void writeToFile(InputStream inputStream, Path path) { + mkParentDirs(path); + try (FileOutputStream out = new FileOutputStream(path.toFile())) { + byte[] buf = new byte[8192]; + int length; + while ((length = inputStream.read(buf)) > 0) { + out.write(buf, 0, length); + } + } catch (IOException e) { + throw new PlaywrightException("Failed to write to file", e); + } + } + static boolean isSafeCloseError(PlaywrightException exception) { return isSafeCloseError(exception.getMessage()); } diff --git a/playwright/src/main/java/com/microsoft/playwright/impl/WebSocketTransport.java b/playwright/src/main/java/com/microsoft/playwright/impl/WebSocketTransport.java new file mode 100644 index 00000000..d85aafa3 --- /dev/null +++ b/playwright/src/main/java/com/microsoft/playwright/impl/WebSocketTransport.java @@ -0,0 +1,100 @@ +/* + * Copyright (c) Microsoft Corporation. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.microsoft.playwright.impl; + +import com.microsoft.playwright.PlaywrightException; +import org.java_websocket.client.WebSocketClient; +import org.java_websocket.handshake.ServerHandshake; + +import java.io.IOException; +import java.net.URI; +import java.time.Duration; +import java.util.concurrent.ArrayBlockingQueue; +import java.util.concurrent.BlockingQueue; +import java.util.concurrent.TimeUnit; + + +class WebSocketTransport implements Transport { + private final BlockingQueue incoming = new ArrayBlockingQueue<>(1000); + private final ClientConnection clientConnection; + private boolean isClosed; + private volatile Exception lastError; + + private class ClientConnection extends WebSocketClient { + ClientConnection(URI serverUri) { + super(serverUri); + } + + @Override + public void onOpen(ServerHandshake handshakedata) { + } + + @Override + public void onMessage(String message) { + incoming.add(message); + } + + @Override + public void onClose(int code, String reason, boolean remote) { + } + + @Override + public void onError(Exception ex) { + lastError = ex; + } + } + + WebSocketTransport(URI uri, Duration timeout) { + clientConnection = new ClientConnection(uri); + try { + if (!clientConnection.connectBlocking(timeout.toMillis(), TimeUnit.MILLISECONDS)) { + throw new PlaywrightException("Failed to connect", lastError); + } + } catch (InterruptedException e) { + throw new PlaywrightException("Failed to connect", e); + } + } + + @Override + public void send(String message) { + if (clientConnection.isClosed()) { + throw new PlaywrightException("Playwright connection closed"); + } + clientConnection.send(message); + } + + @Override + public String poll(Duration timeout) { + if (isClosed || clientConnection.isClosed()) { + throw new PlaywrightException("Playwright connection closed"); + } + try { + return incoming.poll(timeout.toMillis(), TimeUnit.MILLISECONDS); + } catch (InterruptedException e) { + throw new PlaywrightException("Failed to read message", e); + } + } + + @Override + public void close() throws IOException { + if (isClosed) { + return; + } + isClosed = true; + clientConnection.close(); + } +} diff --git a/pom.xml b/pom.xml index 2ca127f3..d8458353 100644 --- a/pom.xml +++ b/pom.xml @@ -76,7 +76,6 @@ org.java-websocket Java-WebSocket ${websocket.version} - test diff --git a/scripts/CLI_VERSION b/scripts/CLI_VERSION index 2c34c074..d6d01125 100644 --- a/scripts/CLI_VERSION +++ b/scripts/CLI_VERSION @@ -1 +1 @@ -1.9.1-1614654987000 +1.9.1-1614734424000