java/com/jetbrains/cef/remote/browser/RemoteBrowser.java (740 lines of code) (raw):

package com.jetbrains.cef.remote.browser; import com.jetbrains.cef.remote.PlatformUtils; import com.jetbrains.cef.remote.RpcContext; import com.jetbrains.cef.remote.callback.RemoteIntCallback; import com.jetbrains.cef.remote.callback.RemotePdfPrintCallback; import com.jetbrains.cef.remote.callback.RemoteRunFileDialogCallback; import com.jetbrains.cef.remote.callback.RemoteStringVisitor; import com.jetbrains.cef.remote.network.RemoteRequest; import com.jetbrains.cef.remote.network.RemoteRequestContext; import com.jetbrains.cef.remote.network.RemoteRequestImpl; import com.jetbrains.cef.remote.thrift_codegen.CompositionUnderline; import com.jetbrains.cef.remote.thrift_codegen.RObject; import com.jetbrains.cef.remote.thrift_codegen.Range; import com.jetbrains.cef.remote.thrift_codegen.Style; import org.cef.CefBrowserSettings; import org.cef.CefClient; import org.cef.browser.*; import org.cef.callback.CefPdfPrintCallback; import org.cef.callback.CefRunFileDialogCallback; import org.cef.callback.CefStringVisitor; import org.cef.handler.CefDialogHandler; import org.cef.handler.CefNativeRenderHandler; import org.cef.handler.CefRenderHandler; import org.cef.handler.CefWindowHandler; import org.cef.input.CefCompositionUnderline; import org.cef.input.CefTouchEvent; import org.cef.misc.CefLog; import org.cef.misc.CefPdfPrintSettings; import org.cef.misc.CefRange; import org.cef.network.CefRequest; import java.awt.*; import java.awt.event.KeyEvent; import java.awt.event.MouseEvent; import java.awt.event.MouseWheelEvent; import java.awt.image.BufferedImage; import java.util.*; import java.util.List; import java.util.concurrent.CompletableFuture; import java.util.concurrent.atomic.AtomicBoolean; import java.util.function.Supplier; public class RemoteBrowser implements CefBrowser { private final RpcContext myRpc; private final RemoteClient myOwner; private final CefClient myCefClient; // will be the "owner" of RemoteClient, needed to override getClient() private final RemoteRequestContext myRequestContext; private final CefBrowserSettings mySettings; // TODO: use settings in startNativeCreation private volatile int myBid = -1; private String myUrl = null; private Component myComponent; private CefNativeRenderHandler myRender; private final AtomicBoolean myIsNativeBrowserCreationRequested = new AtomicBoolean(false); private final AtomicBoolean myIsNativeBrowserCreationStarted = new AtomicBoolean(false); private volatile boolean myIsNativeBrowserCreated = false; private volatile boolean myIsClosing = false; private volatile boolean myIsClosed = false; private volatile int myNativeBrowserIdentifier = Integer.MIN_VALUE; private final List<Runnable> myDelayedActions = new ArrayList<>(); private int myFrameRate = 30; // just for cache private volatile boolean myIsDevToolsOpened = false; private volatile CefDevToolsClient myDevToolsClient = null; private Point myInspectPoint; RemoteBrowser(RpcContext rpcContext, RemoteClient owner, CefClient cefClient, String url, RemoteRequestContext requestContext, CefBrowserSettings settings) { myRpc = rpcContext; myOwner = owner; myCefClient = cefClient; myUrl = url; myRequestContext = requestContext != null ? requestContext : new RemoteRequestContext(myRpc.server); mySettings = settings; } public int getBid() { return myBid; } public int getCid() { return myOwner.getCid(); } public RemoteClient getOwner() { return myOwner; } public boolean isNativeBrowserCreationStarted() { return myIsNativeBrowserCreationStarted.get(); } public boolean isNativeBrowserCreated() { return myIsNativeBrowserCreated; } public int getNativeBrowserIdentifier() { return myNativeBrowserIdentifier; } protected void setNativeBrowserCreated(int nativeBrowserIdentifier) { // Called from lifespan-handler::onAfterCreated (of owner) synchronized (myDelayedActions) { myIsNativeBrowserCreated = true; myNativeBrowserIdentifier = nativeBrowserIdentifier; myDelayedActions.forEach(r -> r.run()); myDelayedActions.clear(); } } public void setComponent(Component component, CefNativeRenderHandler renderHandler) { myComponent = component; myRender = renderHandler; } private void execWhenCreated(Runnable runnable, String name) { synchronized (myDelayedActions) { if (myIsNativeBrowserCreated) { runnable.run(); } else { CefLog.Debug("%s: add delayed action %s", this, name); myDelayedActions.add(runnable); } } } @Override public void createImmediately() { if (!myIsNativeBrowserCreationRequested.getAndSet(true)) myRpc.server.onConnected(this::requestBid, "requestBid", false); } private void requestBid() { synchronized (myIsNativeBrowserCreationStarted) { if (myIsClosing) return; myIsNativeBrowserCreationStarted.set(true); myOwner.requestCid(); myRpc.exec((s) -> { RObject contextHandler = new RObject(); if (myRequestContext.getRemoteHandler() != null) contextHandler = myRequestContext.getRemoteHandler().thriftId(); myBid = s.Browser_Create(myOwner.getCid(), contextHandler); }); if (myBid >= 0) { myRpc.server.bid2Browser.put(myBid, this); CefLog.Debug("Registered bid %d", myBid); // At current point new bid is registered so java-handlers calls will be dispatched correctly. // We can't start creation earlier because for example onAfterCreated can be called before new bid is registered. myRpc.exec((s) -> s.Browser_StartNativeCreation(myBid, myUrl)); } else CefLog.Error("Can't obtain bid, createBrowser returns %d", myBid); } if (myBid >= 0) myRequestContext.setBid(myBid, myRpc); } @Override public Component getUIComponent() { return myComponent; } @Override public CefClient getClient() { return myCefClient; } @Override public CefRequestContext getRequestContext() { return myRequestContext; } @Override public CefRenderHandler getRenderHandler() { return myRender; } @Override public CefWindowHandler getWindowHandler() { // Remote mode uses OSR only. return null; } @Override public boolean canGoBack() { if (myIsClosing || myBid < 0) return false; return myRpc.execObj(s-> s.Browser_CanGoBack(myBid)); } @Override public void goBack() { if (myIsClosing || myBid < 0) return; myRpc.invokeLater(s-> s.Browser_GoBack(myBid)); } @Override public boolean canGoForward() { if (myIsClosing || myBid < 0) return false; return myRpc.execObj(s-> s.Browser_CanGoForward(myBid)); } @Override public void goForward() { if (myIsClosing || myBid < 0) return; myRpc.invokeLater(s-> s.Browser_GoForward(myBid)); } @Override public boolean isLoading() { if (myIsClosing || myBid < 0) return false; return myRpc.execObj(s-> s.Browser_IsLoading(myBid)); } @Override public void reload() { if (myIsClosing) return; execWhenCreated(()->{ myRpc.invokeLater((s)->{ s.Browser_Reload(myBid); }); }, "reload"); } @Override public void reloadIgnoreCache() { if (myIsClosing) return; execWhenCreated(()->{ myRpc.invokeLater((s)->{ s.Browser_ReloadIgnoreCache(myBid); }); }, "reloadIgnoreCache"); } @Override public void stopLoad() { if (myIsClosing || myBid < 0) return; myRpc.invokeLater(s-> s.Browser_StopLoad(myBid)); } @Override public int getIdentifier() { return myNativeBrowserIdentifier; } @Override public CefFrame getMainFrame() { if (myIsClosing) return null; if (myBid < 0) { CefLog.Debug("bid wasn't received yet and getMainFrame will return null."); return null; } RObject rf = myRpc.execObj(s-> s.Browser_GetMainFrame(myBid)); return rf == null || rf.isNull ? null : new RemoteFrame(myRpc, rf); } @Override public CefFrame getFocusedFrame() { if (myIsClosing) return null; if (myBid < 0) { CefLog.Debug("bid wasn't received yet and getFocusedFrame will return null."); return null; } RObject rf = myRpc.execObj(s-> s.Browser_GetFocusedFrame(myBid)); return rf == null || rf.isNull ? null : new RemoteFrame(myRpc, rf); } @Override public CefFrame getFrameByIdentifier(String identifier) { if (myIsClosing) return null; if (myBid < 0) { CefLog.Debug("bid wasn't received yet and getFrameByIdentifier will return null."); return null; } RObject rf = myRpc.execObj(s-> s.Browser_GetFrameByIdentifier(myBid, identifier)); return rf == null || rf.isNull ? null : new RemoteFrame(myRpc, rf); } @Override public CefFrame getFrameByName(String name) { if (myIsClosing) return null; if (myBid < 0) { CefLog.Debug("bid wasn't received yet and getFrameByName will return null."); return null; } RObject rf = myRpc.execObj(s-> s.Browser_GetFrameByName(myBid, name)); return rf == null || rf.isNull ? null : new RemoteFrame(myRpc, rf); } @Override public Vector<String> getFrameIdentifiers() { if (myIsClosing) return null; if (myBid < 0) { CefLog.Debug("bid wasn't received yet and getFrameIdentifiers will return null."); return null; } List<String> ids = myRpc.execObj(s-> s.Browser_GetFrameIdentifiers(myBid)); return ids == null || ids.isEmpty() ? null : new Vector<>(ids); } @Override public Vector<String> getFrameNames() { if (myIsClosing) return null; if (myBid < 0) { CefLog.Debug("bid wasn't received yet and getFrameNames will return null."); return null; } List<String> ids = myRpc.execObj(s-> s.Browser_GetFrameNames(myBid)); return ids == null || ids.isEmpty() ? null : new Vector<>(ids); } @Override public int getFrameCount() { if (myIsClosing) return 0; if (myBid < 0) { CefLog.Debug("bid wasn't received yet and getFrameCount will return 0."); return 0; } return myRpc.execObj(s-> s.Browser_GetFrameCount(myBid)); } @Override public boolean isPopup() { if (myIsClosing || myBid < 0) return false; return myRpc.execObj(s-> s.Browser_IsPopup(myBid)); } @Override public boolean hasDocument() { if (myIsClosing || myBid < 0) return false; return myRpc.execObj(s-> s.Browser_HasDocument(myBid)); } @Override public void viewSource() { if (myIsClosing) return; execWhenCreated(()->{ myRpc.invokeLater((s)->{ s.Browser_ViewSource(myBid); }); }, "viewSource"); } @Override public void getSource(CefStringVisitor visitor) { if (myIsClosing || visitor == null) return; execWhenCreated(()->{ myRpc.invokeLater((s)->{ RemoteStringVisitor rvisitor = RemoteStringVisitor.create(visitor); s.Browser_GetSource(myBid, rvisitor.thriftId()); }); }, "getSource"); } @Override public void getText(CefStringVisitor visitor) { if (myIsClosing || visitor == null) return; execWhenCreated(()->{ myRpc.invokeLater((s)->{ RemoteStringVisitor rvisitor = RemoteStringVisitor.create(visitor); s.Browser_GetText(myBid, rvisitor.thriftId()); }); }, "getText"); } @Override public void loadRequest(CefRequest request) { if (myIsClosing) return; if (!(request instanceof RemoteRequest)) { CefLog.Error("Unsupported CefRequest: %s", request); return; } execWhenCreated(() -> { RemoteRequestImpl rr = ((RemoteRequest)request).getImpl(); if (rr != null) { rr.flush(); // just for insurance myRpc.exec((s) -> s.Browser_LoadRequest(myBid, rr.thriftIdWithCache())); } else CefLog.Error("RemoteRequestImpl is null [bid=%d]", myBid); }, "loadRequest"); } @Override public void loadURL(String url) { myUrl = url; if (myIsClosing) return; execWhenCreated(()->{ myRpc.exec((s)->{ s.Browser_LoadURL(myBid, url); }); }, "loadURL"); } @Override public void executeJavaScript(String code, String url, int line) { if (myIsClosing) return; execWhenCreated(()->{ myRpc.exec((s)->{ s.Browser_ExecuteJavaScript(myBid, code, url, line); }); }, "executeJavaScript"); } @Override public String getURL() { if (myBid < 0) { CefLog.Debug("Can't do getURL because bid wasn't created, return cached %s", myUrl); return myUrl; } if (myIsClosing) return myUrl; return myRpc.execObj((s)->{ return s.Browser_GetURL(myBid); }); } @Override public void close(boolean force) { synchronized (myIsNativeBrowserCreationStarted) { if (myIsClosing) return; myIsClosing = true; if (myRender != null) myRender.disposeNativeResources(); if (myBid >= 0) myRpc.invokeLater(s -> s.Browser_Close(myBid)); } synchronized (myDelayedActions) { myDelayedActions.clear(); } } @Override public void setCloseAllowed() {} @Override public boolean doClose() { return false; } @Override public void onBeforeClose() { // Called from lifespan handler (before native browser disposed). myIsClosed = true; myRequestContext.dispose(); if (myIsDevToolsOpened) closeDevTools(); if (myDevToolsClient != null) myDevToolsClient.close(); if (myBid >= 0) { RemoteBrowser removed = myRpc.server.bid2Browser.remove(myBid); if (removed == null) CefLog.Error("Unregister bid: bid=%d was already removed.", myBid); } else CefLog.Error("Can't unregister invalid bid %d", myBid); } @Override public boolean isClosing() { return myIsClosing; } @Override public boolean isClosed() { return myIsClosed; } @Override public void setFocus(boolean enable) { if (myIsClosing) return; execWhenCreated(()->{ myRpc.invokeLater((s)->{ s.Browser_SetFocus(myBid, enable); }); }, "setFocus"); } @Override public void setWindowVisibility(boolean visible) { // NOTE: doesn't used in OSR mode } @Override public double getZoomLevel() { if (myBid < 0) { CefLog.Debug("Can't do getZoomLevel because bid wasn't created, return 0"); return 0; } if (myIsClosing) return 0; return myRpc.execObj((s)-> s.Browser_GetZoomLevel(myBid)); } @Override public void setZoomLevel(double zoomLevel) { if (myIsClosing) return; execWhenCreated(()->myRpc.invokeLater((s)-> s.Browser_SetZoomLevel(myBid, zoomLevel)), "setZoomLevel"); } @Override public void runFileDialog(CefDialogHandler.FileDialogMode mode, String title, String defaultFilePath, Vector<String> acceptFilters, CefRunFileDialogCallback callback) { if (myIsClosing) return; if (callback == null) { CefLog.Error("Can't run file dialog because callback is null."); return; } RemoteRunFileDialogCallback rcallback = RemoteRunFileDialogCallback.create(callback); final Vector<String> filters = acceptFilters == null ? new Vector<>() : acceptFilters; execWhenCreated(()->{ myRpc.invokeLater((s)->{ s.Browser_RunFileDialog(myBid, mode.name(), title, defaultFilePath, filters, rcallback.thriftId()); }); }, "runFileDialog"); } @Override public void startDownload(String url) { if (myIsClosing) return; execWhenCreated(()->{ myRpc.invokeLater((s)->{ s.Browser_StartDownload(myBid, url); }); }, "startDownload"); } @Override public void print() { if (myIsClosing) return; execWhenCreated(()->{ myRpc.invokeLater((s)->{ s.Browser_Print(myBid); }); }, "print"); } @Override public void printToPDF(String path, CefPdfPrintSettings settings, CefPdfPrintCallback callback) { if (myIsClosing) return; if (callback == null) { CefLog.Error("Can't print to pdf because callback is null."); return; } RemotePdfPrintCallback rcallback = RemotePdfPrintCallback.create(callback); Map<String, String> printSettings = new HashMap<>(); if (settings != null) { printSettings.put("landscape", String.valueOf(settings.landscape)); printSettings.put("print_background", String.valueOf(settings.print_background)); printSettings.put("scale", String.valueOf(settings.scale)); printSettings.put("paper_width", String.valueOf(settings.paper_width)); printSettings.put("paper_height", String.valueOf(settings.paper_height)); printSettings.put("prefer_css_page_size", String.valueOf(settings.prefer_css_page_size)); if (settings.margin_type != null) printSettings.put("margin_type", String.valueOf(settings.margin_type)); printSettings.put("margin_top", String.valueOf(settings.margin_top)); printSettings.put("margin_bottom", String.valueOf(settings.margin_bottom)); printSettings.put("margin_right", String.valueOf(settings.margin_right)); printSettings.put("margin_left", String.valueOf(settings.margin_left)); if (settings.page_ranges != null && !settings.page_ranges.isEmpty()) printSettings.put("page_ranges", settings.page_ranges); printSettings.put("display_header_footer", String.valueOf(settings.display_header_footer)); if (settings.header_template != null && !settings.header_template.isEmpty()) printSettings.put("header_template", settings.header_template); if (settings.footer_template != null && !settings.footer_template.isEmpty()) printSettings.put("footer_template", settings.footer_template); printSettings.put("generate_document_outline", String.valueOf(settings.generate_document_outline)); printSettings.put("generate_tagged_pdf", String.valueOf(settings.generate_tagged_pdf)); } execWhenCreated(()->{ myRpc.invokeLater((s)->{ s.Browser_PrintToPDF(myBid, path, printSettings, rcallback.thriftId()); }); }, "printToPDF"); } @Override public void find(String searchText, boolean forward, boolean matchCase, boolean findNext) { if (myIsClosing) return; execWhenCreated(()->{ myRpc.invokeLater((s)->{ s.Browser_Find(myBid, searchText, forward, matchCase, findNext); }); }, "find"); } @Override public void stopFinding(boolean clearSelection) { if (myIsClosing) return; execWhenCreated(()->{ myRpc.invokeLater((s)->{ s.Browser_StopFinding(myBid, clearSelection); }); }, "stopFinding"); } @Override public void openDevTools() { openDevTools(null); } @Override public void openDevTools(Point inspectAt) { if (myIsClosing) return; if (myBid == -1) { CefLog.Error("Can't open dev-tools because bid is -1"); } else { myRpc.exec((s) -> s.Browser_OpenDevTools(myBid, myInspectPoint != null ? myInspectPoint.x : 0, myInspectPoint != null ? myInspectPoint.y : 0)); } } @Override public void closeDevTools() { execWhenCreated(()->{ myRpc.invokeLater((s)->{ s.Browser_CloseDevTools(myBid); }); }, "closeDevTools"); } @Override public CefDevToolsClient getDevToolsClient() { if (myIsClosing) return null; if (myDevToolsClient == null || myDevToolsClient.isClosed()) myDevToolsClient = new CefDevToolsClient(this); return myDevToolsClient; } @Override public void replaceMisspelling(String word) { if (myIsClosing) return; execWhenCreated(()->{ myRpc.invokeLater((s)->{ s.Browser_ReplaceMisspelling(myBid, word); }); }, "replaceMisspelling"); } @Override public void wasResized(int width/*unused*/, int height/*unused*/) { // NOTE: width, height are unused. // This method will schedule request of new size via CefRenderHandler.GetViewRect. if (myIsClosing) return; execWhenCreated(()->{ myRpc.invokeLater(s -> s.Browser_WasResized(myBid)); }, "wasResized"); } @Override public void invalidate() { if (myIsClosing) return; execWhenCreated(()->{ myRpc.invokeLater(s -> s.Browser_Invalidate(myBid)); }, "invalidate"); } @Override public void notifyScreenInfoChanged() { if (myIsClosing) return; execWhenCreated(()->{ myRpc.invokeLater(s -> s.Browser_NotifyScreenInfoChanged(myBid)); }, "notifyScreenInfoChanged"); } @Override public void sendKeyEvent(KeyEvent e) { if (myBid < 0) { CefLog.Debug("Skip sendKeyEvent because remote browser wasn't created, bid=%d", myBid); return; } if (myIsClosing) return; var cefKeyEvent = PlatformUtils.getCefKeyEventAttributes(e); myRpc.invokeLater(s -> s.Browser_SendCefKeyEvent(myBid, cefKeyEvent)); } @Override public void sendMouseEvent(MouseEvent e) { if (myBid < 0) { CefLog.Debug("Skip sendMouseEvent because remote browser wasn't created, bid=%d", myBid); return; } if (myIsClosing) return; myRpc.invokeLater(s -> s.Browser_SendMouseEvent(myBid, e.getID(), e.getX(), e.getY(), e.getModifiersEx(), e.getClickCount(), e.getButton())); } @Override public void sendMouseWheelEvent(MouseWheelEvent e) { if (myBid < 0) { CefLog.Debug("Skip sendMouseWheelEvent because remote browser wasn't created, bid=%d", myBid); return; } if (myIsClosing) return; myRpc.invokeLater(s -> s.Browser_SendMouseWheelEvent(myBid, e.getScrollType(), e.getX(), e.getY(), e.getModifiersEx(), e.getWheelRotation(), e.getUnitsToScroll())); } @Override public void sendTouchEvent(CefTouchEvent e) { CefLog.Error("UNIMPLEMENTED: sendTouchEvent"); } @Override public CompletableFuture<BufferedImage> createScreenshot(boolean nativeResolution) { return null; } @Override public void ImeSetComposition(String text, List<CefCompositionUnderline> cefUnderlines, CefRange cefReplacementRange, CefRange cefSelectionRange) { List<CompositionUnderline> underlineList = new ArrayList<>(); if (cefUnderlines != null) { for (var cefUnderline : cefUnderlines) { Range range = new Range(cefUnderline.getRange().from, cefUnderline.getRange().to); com.jetbrains.cef.remote.thrift_codegen.Color color = new com.jetbrains.cef.remote.thrift_codegen.Color( cefUnderline.getColor().getRed(), cefUnderline.getColor().getGreen(), cefUnderline.getColor().getBlue(), cefUnderline.getColor().getAlpha()); com.jetbrains.cef.remote.thrift_codegen.Color backgroundColor = new com.jetbrains.cef.remote.thrift_codegen.Color( cefUnderline.getBackgroundColor().getRed(), cefUnderline.getBackgroundColor().getGreen(), cefUnderline.getBackgroundColor().getBlue(), cefUnderline.getBackgroundColor().getAlpha()); Style style = Style.NONE; switch (cefUnderline.getStyle()) { case SOLID -> style = Style.SOLID; case DOT -> style = Style.DOT; case DASH -> style = Style.DASH; } underlineList.add(new CompositionUnderline( range, color, backgroundColor, cefUnderline.getThick(), style)); } } Range replacementRange = new Range(cefReplacementRange.from, cefReplacementRange.to); Range selectionRange = new Range(cefSelectionRange.from, cefSelectionRange.to); myRpc.invokeLater(s -> s.Browser_ImeSetComposition(myBid, text, underlineList, replacementRange, selectionRange)); } @Override public void ImeCommitText(String text, CefRange cefReplacementRange, int relativeCursorPos) { Range replacementRange = new Range(cefReplacementRange.from, cefReplacementRange.to); myRpc.invokeLater(s -> s.Browser_ImeCommitText(myBid, text, replacementRange, relativeCursorPos)); } @Override public void ImeFinishComposingText(boolean b) { myRpc.invokeLater(s -> s.Browser_ImeFinishComposingText(myBid, b)); } @Override public void ImeCancelComposing() { myRpc.invokeLater(s -> s.Browser_ImeCancelComposing(myBid)); } @Override public void setWindowlessFrameRate(int frameRate) { myFrameRate = frameRate; if (myIsClosing) return; execWhenCreated(()->{ myRpc.invokeLater((s)->{ s.Browser_SetFrameRate(myBid, frameRate); }); }, "setWindowlessFrameRate"); } @Override public CompletableFuture<Integer> getWindowlessFrameRate() { CefLog.Warn("%s: getWindowlessFrameRate returns cached value %d. TODO: implement real getWindowlessFrameRate.", this, myFrameRate); CompletableFuture<Integer> result = new CompletableFuture<Integer>(); result.complete(myFrameRate); return result; } @Override public String toString() { return "RemoteBrowser_" + myBid; } public CefRegistration addDevToolsMessageObserver(CefDevToolsMessageObserver observer) { if (myIsClosing || observer == null) return null; if (!myIsNativeBrowserCreated) { CefLog.Error("Can't add DevToolsMessageObserver because native browser wasn't created"); return null; } RemoteDevToolsMessageObserver robserver = RemoteDevToolsMessageObserver.create(observer); RObject registration = myRpc.execObj(s -> s.Browser_AddDevToolsMessageObserver(myBid, robserver.thriftId())); RemoteRegistrationImpl impl = new RemoteRegistrationImpl(myRpc, registration); return new RemoteRegistration(impl); } public CompletableFuture<Integer> executeDevToolsMethod(String method, String parametersAsJson) { CompletableFuture<Integer> future = new CompletableFuture<>(); if (myIsClosing || method == null) { future.completeExceptionally(new CefDevToolsClient.DevToolsException(myIsClosing ? "Client is closing." : "Method is null.")); } else if (!myIsNativeBrowserCreated) { CefLog.Error("Can't execute DevToolsMethod because native browser wasn't created"); future.completeExceptionally(new CefDevToolsClient.DevToolsException("Native browser wasn't created")); } else { RemoteIntCallback ricb = RemoteIntCallback.create(generatedMessageId -> { if (generatedMessageId <= 0) { future.completeExceptionally(new CefDevToolsClient.DevToolsException( String.format("Failed to execute DevTools method %s, generatedMessageId=%d", method, generatedMessageId))); } else { future.complete(generatedMessageId); } }); execWhenCreated(() -> { myRpc.invokeLater(s -> s.Browser_ExecuteDevToolsMethod(myBid, method, parametersAsJson, ricb.thriftId())); }, String.format("executeDevToolsMethod: %s(%s)", method, parametersAsJson)); } return future; } @Override public boolean isWindowless() { return true; } }