java/org/cef/CefApp.java (541 lines of code) (raw):

// Copyright (c) 2013 The Chromium Embedded Framework Authors. All rights // reserved. Use of this source code is governed by a BSD-style license that // can be found in the LICENSE file. package org.cef; import com.jetbrains.cef.JCefAppConfig; import com.jetbrains.cef.remote.CefServer; import com.jetbrains.cef.remote.NativeServerManager; import com.jetbrains.cef.remote.ThriftTransport; import com.jetbrains.cef.remote.browser.RemoteClient; import com.jetbrains.cef.remote.callback.RemoteSchemeHandlerFactory; import org.cef.browser.CefRendering; import org.cef.callback.CefSchemeHandlerFactory; import org.cef.handler.CefAppHandler; import org.cef.handler.CefAppHandlerAdapter; import org.cef.handler.CefAppStateHandler; import org.cef.misc.CefLog; import org.cef.misc.Utils; import javax.swing.*; import java.awt.event.ActionEvent; import java.awt.event.ActionListener; import java.io.File; import java.lang.reflect.InvocationTargetException; import java.util.HashSet; import java.util.LinkedList; import java.util.concurrent.CompletableFuture; import java.util.concurrent.Executor; import java.util.concurrent.atomic.AtomicBoolean; import java.util.function.Supplier; /** * Exposes static methods for managing the global CEF context. */ public class CefApp extends CefAppHandlerAdapter { public class CefVersion { public final int JCEF_COMMIT_NUMBER; public final String JCEF_COMMIT_HASH; public final int CEF_VERSION_MAJOR; public final int CEF_VERSION_MINOR; public final int CEF_VERSION_PATCH; public final int CEF_COMMIT_NUMBER; public final int CHROME_VERSION_MAJOR; public final int CHROME_VERSION_MINOR; public final int CHROME_VERSION_BUILD; public final int CHROME_VERSION_PATCH; protected CefVersion(int jcefCommitNo, String jcefCommitHash, int cefMajor, int cefMinor, int cefPatch, int cefCommitNo, int chrMajor, int chrMin, int chrBuild, int chrPatch) { JCEF_COMMIT_NUMBER = jcefCommitNo; JCEF_COMMIT_HASH = jcefCommitHash; CEF_VERSION_MAJOR = cefMajor; CEF_VERSION_MINOR = cefMinor; CEF_VERSION_PATCH = cefPatch; CEF_COMMIT_NUMBER = cefCommitNo; CHROME_VERSION_MAJOR = chrMajor; CHROME_VERSION_MINOR = chrMin; CHROME_VERSION_BUILD = chrBuild; CHROME_VERSION_PATCH = chrPatch; } public String getJcefVersion() { return CEF_VERSION_MAJOR + "." + CEF_VERSION_MINOR + "." + CEF_VERSION_PATCH + "." + JCEF_COMMIT_NUMBER + "." + JCEF_COMMIT_HASH; } public String getCefVersion() { return CEF_VERSION_MAJOR + "." + CEF_VERSION_MINOR + "." + CEF_VERSION_PATCH; } public String getChromeVersion() { return CHROME_VERSION_MAJOR + "." + CHROME_VERSION_MINOR + "." + CHROME_VERSION_BUILD + "." + CHROME_VERSION_PATCH; } @Override public String toString() { return "JCEF Version = " + getJcefVersion() + "\n" + "CEF Version = " + getCefVersion() + "\n" + "Chromium Version = " + getChromeVersion(); } } /** * The CefAppState gives you a hint if the CefApp is already usable or not * usable any more. See values for details. */ public enum CefAppState { /** * No CefApp instance was created yet. Call getInstance() to create a new * one. */ NONE, /** * CefApp is new created but not initialized yet. No CefClient and no * CefBrowser was created until now. */ NEW, /** * CefApp is in its initializing process. Please wait until initializing is * finished. */ INITIALIZING, /** * CefApp is up and running. At least one CefClient was created and the * message loop is running. You can use all classes and methods of JCEF now. */ INITIALIZED, /** * CefApp is in its shutdown process. All CefClients and CefBrowser * instances will be disposed. No new CefClient or CefBrowser is allowed to * be created. The message loop will be performed until all CefClients and * all CefBrowsers are disposed completely. */ SHUTTING_DOWN, /** * CefApp is terminated and can't be used any more. You can shutdown the * application safely now. */ TERMINATED } /** * According to the singleton pattern, this attribute keeps * one single object of this class. */ private static CefApp self = null; private static CefAppHandler userAppHandler_ = null; private static final CompletableFuture<Void> ourStartupFeature = new CompletableFuture<>(); private final CefAppHandler appHandler_; private CefAppState state_ = CefAppState.NONE; private Timer workTimer_ = null; private final HashSet<CefClient> clients_ = new HashSet<CefClient>(); private CefSettings settings_ = null; private final CefServer server_; // // Background initialization support // private volatile boolean isInitialized_ = false; private final LinkedList<CefAppStateHandler> initializationListeners_ = new LinkedList<>(); // Constants for testing JBR-5530 private static final boolean PREINIT_ON_ANY_THREAD = Utils.getBoolean("jcef_app_preinit_any"); private static final int PREINIT_TEST_DELAY_MS = Utils.getInteger("jcef_app_preinit_test_delay_ms", 0); private static final int INIT_TEST_DELAY_MS = Utils.getInteger("jcef_app_init_test_delay_ms", 0); private static boolean isRemoteEnabled_; static { if (!isRemoteSupported()) { isRemoteEnabled_ = Boolean.getBoolean("jcef.remote.force_enabled"); // possibility for debugging if (isRemoteEnabled_) System.out.println("JCEF is forced to be enabled."); } else isRemoteEnabled_ = !Boolean.getBoolean("jcef.remote.disabled"); } /** * To get an instance of this class, use the method * getInstance() instead of this CTOR. * <p> * The CTOR is called by getInstance() as needed and * loads all required JCEF libraries. */ private CefApp(String[] args, CefSettings settings, CefServer server) { super(args); appHandler_ = userAppHandler_; userAppHandler_ = null; if (settings != null) settings_ = settings.clone(); setState(CefAppState.NEW); server_ = server; if (server_ != null) { server_.setCefApp(this); // Perform CefServer initialization in separate thread. new Thread(()->{ if (server_.start(appHandler_)) { CefLog.Debug("%s: native CefServer is initialized.", this); setState(CefAppState.INITIALIZED); synchronized (initializationListeners_) { isInitialized_ = true; initializationListeners_.forEach(l -> l.stateHasChanged(CefAppState.INITIALIZED)); initializationListeners_.clear(); } CefLog.Info("%s: connected to CefServer. JCEF version: %s", this, getVersion()); } }, "CefInitialize-thread").start(); return; } ourStartupFeature.thenRunAsync(() -> { // Perform native pre-initialization. // This code will save global pointer to JVM instance. // Execute on the AWT event dispatching thread to store JNI context from EDT // NOTE: in practice it seems that this method can be called from any thread (at tests // execute successfully) // TODO: ensure and make all initialization steps in single bg thread. preinit(args); initialize(); }, new NamedThreadExecutor("CefInitialize-thread")); } private void preinit(String[] args) throws RuntimeException { // Start preInit AtomicBoolean success = new AtomicBoolean(false); if (PREINIT_ON_ANY_THREAD) success.set(N_PreInitialize()); else { try { SwingUtilities.invokeAndWait(() -> success.set(N_PreInitialize())); } catch (InterruptedException | InvocationTargetException e) { throw new RuntimeException("Failed to do JCEF preinit", e); } } if (!success.get()) { throw new RuntimeException("Failed to do JCEF preinit"); } } // Notifies (in initialization thread) listener that native context has been initialized. // When context is already initialized then listener executes immediately. public void onInitialization(CefAppStateHandler initListener) { onInitialization(initListener, false); } public void onInitialization(CefAppStateHandler initListener, boolean first) { synchronized (initializationListeners_) { if (isInitialized_) initListener.stateHasChanged(CefAppState.INITIALIZED); else { if (first) initializationListeners_.addFirst(initListener); else initializationListeners_.addLast(initListener); } } } /** * Assign an AppHandler to CefApp. The AppHandler can be used to evaluate * application arguments, to register your own schemes and to hook into the * shutdown sequence. See CefAppHandler for more details. * <p> * This method must be called before CefApp is initialized. CefApp will be * initialized automatically if you call createClient() the first time. * * @param appHandler An instance of CefAppHandler. * @throws IllegalStateException in case of CefApp is already initialized */ public static void addAppHandler(CefAppHandler appHandler) throws IllegalStateException { if (self != null) throw new IllegalStateException("Must be called before CefApp is initialized"); userAppHandler_ = appHandler; } public static void setDefaultRenderingFactory(Supplier<CefRendering> factory) { RemoteClient.setDefaultRenderingFactory(factory); } /** * Get an instance of this class. * * @return an instance of this class */ public static synchronized CefApp getInstance() { if (self != null) return self; return getInstance(null, null, null); } @Deprecated(forRemoval = true) public static synchronized CefApp getInstance(String[] args) { return getInstance(args, null, null); } @Deprecated(forRemoval = true) public static synchronized CefApp getInstance(CefSettings settings) { return getInstance(null, settings, null); } public static synchronized CefApp getInstance(String[] args, CefSettings settings, File serverExe) { ThriftTransport st = null; if (isRemoteEnabled()) { st = ThriftTransport.ourDefaultServer; final int count = CefServer.getInstancesCount(); if (count > 0) { if (ThriftTransport.isTcpUsed()) { st = new ThriftTransport(ThriftTransport.findFreePort()); } else { final String suffix = "" + System.currentTimeMillis(); st = new ThriftTransport(ThriftTransport.getServerPipe(suffix)); } CefLog.Debug("CefApp.getInstance: found %d instances of CefServer, so change default server transport %s to %s", count, ThriftTransport.ourDefaultServer, st); } } return getInstance(args, settings, st, serverExe); } public static synchronized CefApp getInstance(CefServer server) { if (isRemoteEnabled()) { CefApp result = new CefApp(server.getArgs(), server.getCefSettings(), server); if (self == null) // Set default instance self = result; return result; } return self; } public static synchronized CefApp getInstance(String[] args, CefSettings settings, ThriftTransport st, File serverExe) { if (isRemoteEnabled()) { // 1. Get command line args (from passed arguments and userAppHandler_) final String[] realArgs; if (args != null && args.length > 0) realArgs = args; else if (userAppHandler_ instanceof CefAppHandlerAdapter) { realArgs = ((CefAppHandlerAdapter)userAppHandler_).getArgs(); } else { if (userAppHandler_ != null) CefLog.Error("Unsupported class of CefAppHandler %s. Overridden command-line arguments will be ignored.", CefAppHandler.class); realArgs = null; } // 2. Try to find the existing instance with the same args and settings. CefServer s = CefServer.findRunningInstance(realArgs, settings); if (s != null) { CefLog.Debug("Found running CefServer instance %s for settings: %s", s.toStringDetailed(), settings.getDescription()); return s.getCefApp(); } // 3. Create new CefApp instance. s = new CefServer(serverExe, st, realArgs, settings); CefApp result = new CefApp(realArgs, settings, s); // 4. Set default instance if (self == null) self = result; return result; } if (settings != null) { if (self != null) throw new IllegalStateException("Settings can only be passed to CEF" + " before createClient is called the first time. Current state is " + self.state_); } if (self == null) self = new CefApp(args, settings, null); else if (self.isTerminated()) throw new IllegalStateException("CefApp was terminated"); return self; } public static synchronized CefApp findRunningInstance(String[] args, CefSettings settings) { if (!isRemoteEnabled()) return null; final CefServer s = CefServer.findRunningInstance(args, settings); return s != null ? s.getCefApp() : null; } public static synchronized CefApp getInstanceIfAny() { return self; } public static synchronized void setDefaultInstance(CefApp cefApp) { self = cefApp; } public final synchronized void setSettings(CefSettings settings) throws IllegalStateException { if (state_.compareTo(CefAppState.NEW) > 0) throw new IllegalStateException("Settings can only be passed to CEF" + " before createClient is called the first time. Current state is " + state_); settings_ = settings.clone(); } public void setDisconnectionCallback(Runnable disconnectionCallback) { if (server_ != null) server_.setDisconnectionCallback(disconnectionCallback); else CefLog.Error("setDisconnectionCallback is not supported for in-process CEF instance"); } public final CefVersion getVersion() { if (server_ != null) { // TODO: request remaining params from server return new CefVersion(0, "0", 0, 0, 0, 0, 0, 0, 0, 0) { @Override public String toString() { return "remote_" + server_.getVersion(); } @Override public String getJcefVersion() { return "remote_" + server_.getVersion(); } }; } try { return N_GetVersion(); } catch (UnsatisfiedLinkError ule) { CefLog.Error("Failed to get CEF version. %s", ule.getMessage()); } return null; } public static final boolean isRemoteSupported() { return NativeServerManager.isRemoteSupported(); } public static void setIsRemoteEnabled(boolean value) { isRemoteEnabled_ = value; } public static boolean isRemoteEnabled() { return isRemoteEnabled_; } public final CefServer getServer() { return server_; } @Override public String toString() { if (server_ == null) return "CefApp"; return "CefApp(" + server_.toStringShort() + ")"; } /** * Returns the current state of CefApp. * * @return current state. */ public static CefAppState getState() { final CefApp app = self; return app == null ? CefAppState.NONE : app.state_; } public synchronized boolean isTerminated() { return state_ == CefAppState.TERMINATED; } public synchronized boolean isShuttingDown() { return state_ == CefAppState.TERMINATED || state_ == CefAppState.SHUTTING_DOWN; } private synchronized void setState(final CefAppState state) { if (state.compareTo(state_) < 0) { String errMsg = "" + this + ": state cannot go backward. Current state " + state_ + ". Proposed state " + state; CefLog.Error(errMsg); throw new IllegalStateException(errMsg); } CefLog.Info("%s: set state %s", this, state); state_ = state; // Execute on the AWT event dispatching thread. SwingUtilities.invokeLater(new Runnable() { @Override public void run() { CefAppHandler handler = appHandler_ == null ? CefApp.this : appHandler_; if (handler != null) handler.stateHasChanged(state); } }); } /** * To shutdown the system, it's important to call the dispose * method. Calling this method closes all client instances with * and all browser instances each client owns. After that the * message loop is terminated and CEF is shutdown. */ public synchronized final void dispose() { switch (state_) { case NEW: // Nothing to do inspite of invalidating the state setState(CefAppState.TERMINATED); break; case INITIALIZING: case INITIALIZED: // (3) Shutdown sequence. Close all clients and continue. setState(CefAppState.SHUTTING_DOWN); if (clients_.isEmpty()) { finishShutdown(); } else { // shutdown() will be called from clientWasDisposed() when the last // client is gone. // Use a copy of the HashSet to avoid iterating during modification. CefLog.Debug("%s: dispose clients before shutting down", this); HashSet<CefClient> clients = new HashSet<CefClient>(clients_); for (CefClient c : clients) { CefLog.Debug("%s: dispose client %s", this, c); c.dispose(); } } break; case NONE: case SHUTTING_DOWN: case TERMINATED: // Ignore shutdown, CefApp is already terminated, in shutdown progress // or was never created (shouldn't be possible) break; } } /** * Creates a new client instance and returns it to the caller. * One client instance is responsible for one to many browser * instances * * @return a new client instance */ public synchronized CefClient createClient() { if (state_.compareTo(CefAppState.SHUTTING_DOWN) >= 0) { String errMsg = "" + this + ": can't create client in state " + state_; CefLog.Error(errMsg); throw new IllegalStateException(errMsg); } CefClient client = server_ != null ? new CefClient(server_.createClient()) : new CefClient(); onInitialization(client, true); clients_.add(client); return client; } /** * Register a scheme handler factory for the specified |scheme_name| and * optional |domain_name|. An empty |domain_name| value for a standard scheme * will cause the factory to match all domain names. The |domain_name| value * will be ignored for non-standard schemes. If |scheme_name| is a built-in * scheme and no handler is returned by |factory| then the built-in scheme * handler factory will be called. If |scheme_name| is a custom scheme then * also implement the CefApp::OnRegisterCustomSchemes() method in all * processes. This function may be called multiple times to change or remove * the factory that matches the specified |scheme_name| and optional * |domain_name|. Returns false if an error occurs. This function may be * called on any thread in the browser process. */ public boolean registerSchemeHandlerFactory(String schemeName, String domainName, CefSchemeHandlerFactory factory) { if (server_ != null) { server_.onConnected(()->{ RemoteSchemeHandlerFactory rf = RemoteSchemeHandlerFactory.create(factory); server_.exec(s -> s.SchemeHandlerFactory_Register(schemeName, domainName, rf.thriftId())); }, "registerSchemeHandlerFactory", true); return true; } onInitialization(state -> { if (!N_RegisterSchemeHandlerFactory(schemeName, domainName, factory)) CefLog.Error("Can't register scheme [%s:%s]", schemeName, domainName); }); return true; } /** * Clear all registered scheme handler factories. Returns false on error. This * function may be called on any thread in the browser process. */ public boolean clearSchemeHandlerFactories() { if (server_ != null) { server_.onConnected(()->{ server_.exec(s -> s.ClearAllSchemeHandlerFactories()); }, "clearSchemeHandlerFactories", false); return true; } if (!isInitialized_) return false; return N_ClearSchemeHandlerFactories(); } /** * This method is called by a CefClient if it was disposed. This causes * CefApp to clean up its list of available client instances. If all clients * are disposed, CefApp will be shutdown. * * @param client the disposed client. */ protected final synchronized void clientWasDisposed(CefClient client) { clients_.remove(client); synchronized (initializationListeners_) { initializationListeners_.remove(client); } CefLog.Debug("%s: client was disposed: %s [clients count %d]", this, client, clients_.size()); if (clients_.isEmpty() && state_.compareTo(CefAppState.SHUTTING_DOWN) >= 0) { // Shutdown native system. finishShutdown(); } } private static void testSleep(int ms) { if (ms > 0) { CefLog.Debug("testSleep %s ms", ms); try { Thread.sleep(ms); } catch (InterruptedException ignored) { } } } /** * Initialize the context. Can be executed in any thread. */ private void initialize() { setState(CefAppState.INITIALIZING); testSleep(INIT_TEST_DELAY_MS); String logMsg = "Initialize CefApp on " + Thread.currentThread(); if (settings_.log_severity == CefSettings.LogSeverity.LOGSEVERITY_INFO || settings_.log_severity == CefSettings.LogSeverity.LOGSEVERITY_VERBOSE) CefLog.Info(logMsg); else CefLog.Debug(logMsg); CefSettings settings = settings_ != null ? settings_ : new CefSettings(); boolean success = N_Initialize(appHandler_ == null ? this : appHandler_, settings, false); if (success) { CefLog.Debug("CefApp: native initialization is finished."); setState(CefAppState.INITIALIZED); synchronized (initializationListeners_) { isInitialized_ = true; initializationListeners_.forEach(l -> l.stateHasChanged(CefAppState.INITIALIZED)); initializationListeners_.clear(); } CefLog.Info("version: %s | settings: %s", getVersion(), settings.getDescription()); } else CefLog.Error("CefApp: N_Initialize failed."); } /** * Shut down the context. */ private void finishShutdown() { if (server_ != null) { server_.stop(); setState(CefAppState.TERMINATED); return; } new Thread(() -> { // Empiric observation: to avoid crashes we must perform native shutdown in next manner: // last client closed -> small pause (to allow CEF finish some bg tasks) -> call CefShutdown // This small pause will be in bg thread, see JBR-5822. final int sleepBeforeShutdown = Utils.getInteger("JCEF_SLEEP_BEFORE_SHUTDOWN", 999); if (sleepBeforeShutdown > 0) { try { CefLog.Debug("Sleep before native shutdown for %d ms, thread %s.", sleepBeforeShutdown, Thread.currentThread()); Thread.sleep(sleepBeforeShutdown); } catch (InterruptedException e) {} } // Shutdown native CEF. CefLog.Info("Perform native shutdown of CEF on thread %s.", Thread.currentThread()); N_Shutdown(); setState(CefAppState.TERMINATED); }, "CEF-shutdown-thread").start(); } /** * Perform a single message loop iteration. Used on all platforms except * Windows with windowed rendering. */ public final void doMessageLoopWork(final long delay_ms) { if (server_ != null) { CefLog.Error("doMessageLoopWork musn't be called in remote mode."); return; } // Execute on the AWT event dispatching thread. SwingUtilities.invokeLater(new Runnable() { @Override public void run() { if (isTerminated()) return; // The maximum number of milliseconds we're willing to wait between // calls to DoMessageLoopWork(). final long kMaxTimerDelay = 1000 / 30; // 30fps if (workTimer_ != null) { workTimer_.stop(); workTimer_ = null; } if (delay_ms <= 0) { // Execute the work immediately. N_DoMessageLoopWork(); // Schedule more work later. doMessageLoopWork(kMaxTimerDelay); } else { long timer_delay_ms = delay_ms; // Never wait longer than the maximum allowed time. if (timer_delay_ms > kMaxTimerDelay) timer_delay_ms = kMaxTimerDelay; workTimer_ = new Timer((int) timer_delay_ms, new ActionListener() { @Override public void actionPerformed(ActionEvent evt) { // Timer has timed out. workTimer_.stop(); workTimer_ = null; N_DoMessageLoopWork(); // Schedule more work later. doMessageLoopWork(kMaxTimerDelay); } }); workTimer_.start(); } } }); } /** * This method must be called at the beginning of the main() method to perform platform-specific startup * initialization. On Linux this initializes Xlib multithreading and on macOS this dynamically loads the * CEF framework. Can be executed in any thread. * * @param args Command-line arguments were used only to get CEF framework path in OSX. */ public static boolean startup(String[] args) { if (isRemoteEnabled()) return true; if (OS.isMacintosh() && args.length == 0) { // this condition is to be removed after adapting IJ startupAsync(JCefAppConfig.getJbrFrameworkPathOSX()); return true; } String frameworkPathOSX = null; String switchPrefix = "--framework-dir-path="; for (String arg : args) { if (arg.startsWith(switchPrefix)) { frameworkPathOSX = new File(arg.substring(switchPrefix.length())).getAbsolutePath(); } } startupAsync(frameworkPathOSX); return true; } /** * @deprecated Use @{@link CefApp#startupAsync(String)}. To be removed in 242 */ @Deprecated(forRemoval = true) public static void startupAsync() { startupAsync(JCefAppConfig.getJbrFrameworkPathOSX()); } /** * Asynchronously starts up the CEF framework. * <p> * This method starts up the CEF framework on a separate thread, allowing the application to continue * executing while the framework is being initialized. Once the framework is initialized, the * operation is considered complete and any registered completion handlers are invoked. * <p> * This method must be called at the beginning of the main() method to perform platform-specific startup * initialization. On Linux this initializes Xlib multithreading and on macOS this dynamically loads the * CEF framework. Can be executed in any thread. * <p> * If the remote debugging feature is enabled, the method returns without actually starting up the * framework. However, the method must be called anyway to unblock further initialization. * * @param frameworkPath The path to the CEF framework on the system. * Used only on macOS. */ public static void startupAsync(String frameworkPath) { if (isRemoteEnabled()) return; new NamedThreadExecutor("CefStartup-thread").execute(()->{ try { if (OS.isWindows()) { // [tav] "jawt" is loaded by JDK AccessBridgeLoader that leads to UnsatisfiedLinkError try { SystemBootstrap.loadLibrary("jawt"); } catch (UnsatisfiedLinkError e) { CefLog.Error("can't load jawt library: " + e.getMessage()); } SystemBootstrap.loadLibrary("chrome_elf"); SystemBootstrap.loadLibrary("libcef"); } else if (OS.isLinux()) { SystemBootstrap.loadLibrary("cef"); } SystemBootstrap.loadLibrary("jcef"); CefApp.N_Startup(frameworkPath); ourStartupFeature.complete(null); } catch (Throwable e) { ourStartupFeature.completeExceptionally(e); } }); } static class NamedThreadExecutor implements Executor { private final String name; NamedThreadExecutor(String name) { this.name = name; } @Override public void execute(Runnable command) { new Thread(command, name).start(); } } private static native boolean N_Startup(String pathToCefFramework); private native boolean N_PreInitialize(); private native boolean N_Initialize(CefAppHandler appHandler, CefSettings settings, boolean checkThread); private native void N_Shutdown(); private native void N_DoMessageLoopWork(); private native CefVersion N_GetVersion(); private native boolean N_RegisterSchemeHandlerFactory( String schemeName, String domainName, CefSchemeHandlerFactory factory); private native boolean N_ClearSchemeHandlerFactories(); }