Files
openide/platform/ui.jcef/jcef/JBCefApp.java
Vladimir Kharitonov 7841791008 IJPL-172117 JCEF: disable JCEF if glibc < 2.28
GitOrigin-RevId: 10d547d74132c4567ce1a2c615289c3f265176de
2024-11-25 10:34:59 +00:00

618 lines
22 KiB
Java

// Copyright 2000-2024 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
package com.intellij.ui.jcef;
import com.intellij.ide.BrowserUtil;
import com.intellij.ide.IdeBundle;
import com.intellij.notification.*;
import com.intellij.openapi.Disposable;
import com.intellij.openapi.actionSystem.AnAction;
import com.intellij.openapi.application.ApplicationManager;
import com.intellij.openapi.diagnostic.Logger;
import com.intellij.openapi.progress.Cancellation;
import com.intellij.openapi.util.Disposer;
import com.intellij.openapi.util.SystemInfo;
import com.intellij.openapi.util.SystemInfoRt;
import com.intellij.openapi.util.Version;
import com.intellij.openapi.util.registry.RegistryManager;
import com.intellij.ui.JreHiDpiUtil;
import com.intellij.ui.scale.DerivedScaleType;
import com.intellij.ui.scale.ScaleContext;
import com.intellij.util.ArrayUtil;
import com.jetbrains.cef.JCefAppConfig;
import com.jetbrains.cef.JCefVersionDetails;
import org.cef.CefApp;
import org.cef.CefClient;
import org.cef.CefSettings;
import org.cef.browser.CefMessageRouter;
import org.cef.callback.CefSchemeHandlerFactory;
import org.cef.callback.CefSchemeRegistrar;
import org.cef.handler.CefAppHandlerAdapter;
import org.cef.misc.BoolRef;
import org.jdom.IllegalDataException;
import org.jetbrains.annotations.ApiStatus;
import org.jetbrains.annotations.Contract;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import javax.swing.*;
import java.awt.*;
import java.io.IOException;
import java.lang.reflect.Method;
import java.net.URL;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.List;
import java.util.*;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.function.Consumer;
import java.util.function.Function;
import java.util.stream.Stream;
import static com.intellij.ui.paint.PaintUtil.RoundingMode.ROUND;
/**
* A wrapper over {@link CefApp}.
* <p>
* Use {@link #getInstance()} to get the app (triggers CEF startup on first call).
* Use {@link #createClient()} to create a client.
*
* @see <a href="https://plugins.jetbrains.com/docs/intellij/jcef.html">Embedded Browser (JCEF) (IntelliJ Platform Docs)</a>
*/
public final class JBCefApp {
private static final Logger LOG = Logger.getInstance(JBCefApp.class);
private static final boolean SKIP_VERSION_CHECK = Boolean.getBoolean("ide.browser.jcef.skip_version_check");
private static final boolean SKIP_MODULE_CHECK = Boolean.getBoolean("ide.browser.jcef.skip_module_check");
private static final boolean IS_REMOTE_ENABLED;
private static final int MIN_SUPPORTED_CEF_MAJOR_VERSION = 119;
private static final int MIN_SUPPORTED_JCEF_API_MAJOR_VERSION = 1;
private static final int MIN_SUPPORTED_JCEF_API_MINOR_VERSION = 13;
private final @Nullable CefDelegate myDelegate;
private final @Nullable CefApp myCefApp;
private final @Nullable CefSettings myCefSettings;
private final @NotNull CompletableFuture<Integer> myDebuggingPort = new CompletableFuture<>();
private final @NotNull Disposable myDisposable = new Disposable() {
@Override
public void dispose() {
if (myCefApp != null) {
myCefApp.dispose();
}
}
};
private static volatile AtomicBoolean ourSupported;
private static final Object ourSupportedLock = new Object();
private static final AtomicBoolean ourInitialized = new AtomicBoolean(false);
private static final List<JBCefCustomSchemeHandlerFactory> ourCustomSchemeHandlerFactoryList =
Collections.synchronizedList(new ArrayList<>());
static {
addCefCustomSchemeHandlerFactory(new JBCefSourceSchemeHandlerFactory());
addCefCustomSchemeHandlerFactory(new JBCefFileSchemeHandlerFactory());
if (RegistryManager.getInstance().is("ide.browser.jcef.out-of-process.enabled")) {
if (Boolean.getBoolean("idea.debug.mode"))
LOG.debug("Out-of-process jcef mode is disabled (because idea.debug.mode=true)");
else if (SystemInfo.isWayland)
LOG.debug("Out-of-process jcef mode is temporarily disabled in Wayland"); // TODO: fix https://youtrack.jetbrains.com/issue/IJPL-161273
else
System.setProperty("jcef.remote.enabled", "true");
}
Boolean result = null;
try {
// Temporary use reflection to avoid jcef-version increment
Method m = CefApp.class.getMethod("isRemoteEnabled");
result = (boolean)m.invoke(CefApp.class);
} catch (Throwable e) {
LOG.warn(e);
}
IS_REMOTE_ENABLED = result != null && result;
}
private JBCefApp(@NotNull JCefAppConfig config) throws IllegalStateException {
myDelegate = getActiveDelegate();
if (myDelegate != null) {
myCefSettings = null;
myCefApp = null;
myDebuggingPort.completeExceptionally(new UnsupportedOperationException());
}
else {
CefSettings settings = Cancellation.forceNonCancellableSectionInClassInitializer(() -> SettingsHelper.loadSettings(config));
if (SystemInfoRt.isLinux && !settings.no_sandbox) {
ApplicationManager.getApplication().executeOnPooledThread(() -> {
if (JBCefAppArmorUtils.areUnprivilegedUserNameSpacesAllowed()) {
CefApp.startup(ArrayUtil.EMPTY_STRING_ARRAY);
}
else {
Notification notification =
getNotificationGroup()
.createNotification(
IdeBundle.message("notification.content.jcef.unprivileged.userns.restricted.title"),
IdeBundle.message("notification.content.jcef.unprivileged.userns.restricted.message"),
NotificationType.WARNING);
AnAction installProfileAction = JBCefAppArmorUtils.getInstallInstallAppArmorProfileAction(() -> notification.expire());
if (installProfileAction != null) {
notification.addAction(installProfileAction);
}
notification.addAction(
NotificationAction.createSimple(
IdeBundle.message("notification.content.jcef.unprivileged.userns.restricted.action.disable.sandbox"),
() -> {
RegistryManager.getInstance().get("ide.browser.jcef.sandbox.enable").setValue(false);
notification.expire();
ApplicationManager.getApplication().restart();
})
);
notification.addAction(
NotificationAction.createSimple(
IdeBundle.message("notification.content.jcef.unprivileged.userns.restricted.action.learn.more"),
() -> {
// TODO(kharitonov): move to https://intellij-support.jetbrains.com/hc/en-us/sections/201620045-Troubleshooting
BrowserUtil.browse("https://youtrack.jetbrains.com/articles/JBR-A-11");
})
);
Notifications.Bus.notify(notification);
}
});
}
else {
CefApp.startup(ArrayUtil.EMPTY_STRING_ARRAY);
}
BoolRef trackGPUCrashes = new BoolRef(false);
String[] args = Cancellation.forceNonCancellableSectionInClassInitializer(() -> SettingsHelper.loadArgs(config, settings, trackGPUCrashes));
CefApp.addAppHandler(new MyCefAppHandler(args, trackGPUCrashes.get()));
myCefSettings = settings;
myCefApp = CefApp.getInstance(settings);
if (myCefSettings.remote_debugging_port > 0) {
myDebuggingPort.complete(myCefSettings.remote_debugging_port);
}
else {
myCefApp.onInitialization(state -> {
try {
myDebuggingPort.complete(readDebugPortFile(Path.of(myCefSettings.cache_path, "DevToolsActivePort")));
}
catch (Exception e) {
myDebuggingPort.completeExceptionally(e);
}
});
}
}
Disposer.register(ApplicationManager.getApplication(), myDisposable);
}
@NotNull
Disposable getDisposable() {
return myDisposable;
}
/**
* Returns {@code JBCefApp} instance.
* <p>
* If the app has not yet been initialized, then it starts up CEF and initializes the app.
*
* @throws IllegalStateException when JCEF initialization is not possible in the current environment
*/
public static @NotNull JBCefApp getInstance() {
if (Holder.INSTANCE == null) {
synchronized (Holder.class) {
if (Holder.INSTANCE == null) {
if (RegistryManager.getInstance().is("ide.browser.jcef.testMode.enabled")) {
// Try again to initialize with probably different registry keys
Holder.INSTANCE = Holder.init();
if (Holder.INSTANCE != null) {
return Objects.requireNonNull(Holder.INSTANCE);
}
}
throw new IllegalStateException("JCEF is not supported in this env or failed to initialize");
}
}
}
return Objects.requireNonNull(Holder.INSTANCE);
}
private static final class Holder {
static volatile @Nullable JBCefApp INSTANCE = init();
static @Nullable JBCefApp init() {
ourInitialized.set(true);
JCefAppConfig config = null;
if (isSupported()) {
try {
if (!JreHiDpiUtil.isJreHiDPIEnabled()) {
System.setProperty("jcef.forceDeviceScaleFactor", String.valueOf(getForceDeviceScaleFactor()));
}
config = JCefAppConfig.getInstance();
}
catch (Exception e) {
LOG.error(e);
}
}
JBCefApp app = null;
if (config != null) {
try {
app = new JBCefApp(config);
}
catch (IllegalStateException ignore) {
}
}
return app;
}
}
/**
* Returns whether JCEF is supported. For that:
* <ul>
* <li>It should be available in the running JBR.</li>
* <li>It should have a compatible version.</li>
* </ul>
* In order to assuredly meet the above requirements, the IDE should run with a bundled JBR.
*/
public static boolean isSupported() {
boolean testModeEnabled = RegistryManager.getInstance().is("ide.browser.jcef.testMode.enabled");
synchronized (ourSupportedLock) {
if (ourSupported != null && !testModeEnabled) {
return ourSupported.get();
}
if (testModeEnabled) {
ourSupported = null;
}
else if (ourSupported != null) {
return ourSupported.get();
}
boolean supported = isSupportedImpl();
ourSupported = new AtomicBoolean(supported);
return supported;
}
}
private static boolean isSupportedImpl() {
CefDelegate delegate = getActiveDelegate();
if (delegate != null) {
return delegate.isCefSupported();
}
if (SystemInfo.isLinux && !isLinuxLibcSupported()) {
return false;
}
Function<String, Boolean> unsupported = (msg) -> {
LOG.warn(msg + (!msg.contains("disabled") ? " (Use JBR bundled with the IDE)" : ""));
return false;
};
// warn: do not change to Registry.is(), the method used at startup
if (!RegistryManager.getInstance().is("ide.browser.jcef.enabled")) {
return unsupported.apply("JCEF is manually disabled via 'ide.browser.jcef.enabled=false'");
}
if (GraphicsEnvironment.isHeadless() &&
!RegistryManager.getInstance().is("ide.browser.jcef.headless.enabled")) {
return unsupported.apply("JCEF is manually disabled in headless env via 'ide.browser.jcef.headless.enabled=false'");
}
if (SystemInfo.isWindows && !SystemInfo.isWin8OrNewer)
return unsupported.apply("JCEF isn't supported in Windows 7");
if (!SKIP_VERSION_CHECK) {
JCefVersionDetails version;
try {
version = JCefAppConfig.getVersionDetails();
}
catch (Throwable e) {
return unsupported.apply("JCEF runtime version is not supported");
}
if (MIN_SUPPORTED_CEF_MAJOR_VERSION > version.cefVersion.major) {
return unsupported.apply("JCEF: minimum supported CEF major version is " + MIN_SUPPORTED_CEF_MAJOR_VERSION +
", current is " + version.cefVersion.major);
}
if (MIN_SUPPORTED_JCEF_API_MAJOR_VERSION > version.apiVersion.major ||
(MIN_SUPPORTED_JCEF_API_MAJOR_VERSION == version.apiVersion.major &&
MIN_SUPPORTED_JCEF_API_MINOR_VERSION > version.apiVersion.minor)) {
return unsupported.apply("JCEF: minimum supported API version is " +
MIN_SUPPORTED_JCEF_API_MAJOR_VERSION + "." + MIN_SUPPORTED_JCEF_API_MINOR_VERSION +
", current is " + version.apiVersion.major + "." + version.apiVersion.minor);
}
}
String altCefPath = System.getProperty("ALT_CEF_FRAMEWORK_DIR", null);
if (altCefPath == null || altCefPath.isEmpty()) {
altCefPath = System.getenv("ALT_CEF_FRAMEWORK_DIR");
}
final boolean skipModuleCheck = (altCefPath != null && !altCefPath.isEmpty()) || SKIP_MODULE_CHECK || IS_REMOTE_ENABLED;
if (!skipModuleCheck) {
URL url = JCefAppConfig.class.getResource("JCefAppConfig.class");
if (url == null) {
return unsupported.apply("JCefAppConfig.class not found");
}
String path = url.toString();
String name = JCefAppConfig.class.getName().replace('.', '/');
boolean isJbrModule = path != null && path.contains("/jcef/" + name);
if (!isJbrModule) {
return unsupported.apply("JCefAppConfig.class is not from a JBR module, url: " + path);
}
}
return true;
}
/**
* Returns {@code true} if JCEF has successfully started.
*/
public static boolean isStarted() {
boolean initialised = ourInitialized.get();
if (!initialised) return false;
//noinspection ConstantConditions
return getInstance() != null;
}
@Contract(pure = true)
@NotNull String getCachePath() {
if (myCefSettings == null) throw new UnsupportedOperationException();
return myCefSettings.cache_path;
}
/**
* @deprecated use {@link JBCefApp#getRemoteDebuggingPort(Consumer)} instead
*/
@Deprecated
@Contract(pure = true)
@NotNull
public Integer getRemoteDebuggingPort() {
if (myCefSettings == null) throw new UnsupportedOperationException();
return myCefSettings.remote_debugging_port;
}
/**
* Schedules passing the debug port number to the consumer once the value is available.
* In case of error, null will be passed to the consumer. The consumer will be called from EDT.
*
* @param consumer - the port number consumer.
*/
public void getRemoteDebuggingPort(Consumer<@Nullable Integer> consumer) {
myDebuggingPort.whenCompleteAsync(
(integer, throwable) -> {
if (throwable != null) {
LOG.error("Failed to get JCEF debugging port: " + throwable.getMessage());
consumer.accept(null);
} else {
consumer.accept(integer);
}
},
f -> SwingUtilities.invokeLater(f)
);
}
public @NotNull JBCefClient createClient() {
CefClient cefClient = myDelegate == null ? Objects.requireNonNull(myCefApp).createClient() : myDelegate.createClient();
return new JBCefClient(cefClient);
}
public @NotNull CefMessageRouter createMessageRouter(@Nullable CefMessageRouter.CefMessageRouterConfig config) {
if (myDelegate != null) {
return myDelegate.createMessageRouter(config);
}
//noinspection SSBasedInspection
return CefMessageRouter.create(config);
}
/**
* Returns {@code true} if the off-screen rendering mode is enabled.
* <p>
* This mode allows for browser creation in either windowed or off-screen rendering mode.
*
* @see JBCefOsrHandlerBrowser
* @see JBCefBrowserBuilder#setOffScreenRendering(boolean)
*/
public static boolean isOffScreenRenderingModeEnabled() {
return SettingsHelper.isOffScreenRenderingModeEnabled();
}
public @Nullable CefSettings getCefSettings() {
return myCefSettings;
}
/**
* Throws {@code IllegalStateException} if the off-screen rendering mode is not enabled.
* <p>
* The off-screen mode allows for browser creation in either windowed or off-screen rendering mode.
*
* @see JBCefOsrHandlerBrowser
* @see JBCefBrowserBuilder#setOffScreenRendering(boolean)
*/
static void checkOffScreenRenderingModeEnabled() {
if (!isOffScreenRenderingModeEnabled()) {
throw new IllegalStateException("off-screen rendering mode is disabled: 'ide.browser.jcef.osr.enabled=false'");
}
}
public static NotificationGroup getNotificationGroup() {
return SettingsHelper.NOTIFICATION_GROUP.getValue();
}
/**
* Adds a custom scheme handler factory.
* <p>
* The method must be called prior to {@code JBCefApp} initialization
* (performed by {@link #getInstance()}). For instance, via the IDE application service.
* <p>
* The method should not be called for built-in schemes ("html", "file", etc.).
*
* @throws IllegalStateException if the method is called after {@code JBCefApp} initialization
*/
/*public*/ static void addCefCustomSchemeHandlerFactory(@NotNull JBCefApp.JBCefCustomSchemeHandlerFactory factory) {
if (ourInitialized.get()) {
throw new IllegalStateException("JBCefApp has already been initialized!");
}
ourCustomSchemeHandlerFactoryList.add(factory);
}
@ApiStatus.Internal
public static List<JBCefCustomSchemeHandlerFactory> getCefCustomSchemeHandlerFactories() {
return Collections.unmodifiableList(ourCustomSchemeHandlerFactoryList);
}
public interface JBCefCustomSchemeHandlerFactory extends CefSchemeHandlerFactory {
/**
* A callback to register the custom scheme handler via calling:
* {@link CefSchemeRegistrar#addCustomScheme(String, boolean, boolean, boolean, boolean, boolean, boolean, boolean)}.
*/
void registerCustomScheme(@NotNull CefSchemeRegistrar registrar);
/**
* Returns the custom scheme name.
*/
@NotNull String getSchemeName();
/**
* Returns a domain name restricting the scheme.
* An empty string should be returned when all domains are permitted.
*/
@NotNull String getDomainName();
}
private static class MyCefAppHandler extends CefAppHandlerAdapter {
private final int myGPUCrashLimit;
private int myGPULaunchCounter = 0;
private boolean myNotificationShown = false;
private final String myArgs;
MyCefAppHandler(String @Nullable [] args, boolean trackGPUCrashes) {
super(args);
myArgs = Arrays.toString(args);
if (trackGPUCrashes) {
myGPUCrashLimit = Integer.getInteger("ide.browser.jcef.gpu.infinitecrash.internallimit", 10);
} else {
myGPUCrashLimit = -1;
}
}
@Override
public boolean onBeforeTerminate() {
// Do not let JCEF auto-terminate by Cmd+Q (or an alternative),
// so that IDE (user) has an option to decide
return true;
}
@Override
public void onRegisterCustomSchemes(CefSchemeRegistrar registrar) {
for (JBCefCustomSchemeHandlerFactory f : ourCustomSchemeHandlerFactoryList) {
f.registerCustomScheme(registrar);
}
}
@Override
public void stateHasChanged(CefApp.CefAppState state) {
if (state.equals(CefApp.CefAppState.INITIALIZED)) {
LOG.info(String.format("jcef version: %s | cmd args: %s", CefApp.getInstance().getVersion().getJcefVersion(), myArgs));
}
}
@Override
public void onContextInitialized() {
for (JBCefCustomSchemeHandlerFactory f : ourCustomSchemeHandlerFactoryList) {
Objects.requireNonNull(getInstance().myCefApp).registerSchemeHandlerFactory(f.getSchemeName(), f.getDomainName(), f);
}
}
@Override
public void onBeforeChildProcessLaunch(String command_line) {
if (command_line == null || !command_line.contains("--type=gpu-process"))
return;
++myGPULaunchCounter;
if (myGPUCrashLimit >= 0 && myGPULaunchCounter > myGPUCrashLimit) {
if (!myNotificationShown) {
ApplicationManager.getApplication().executeOnPooledThread(() -> SettingsHelper.showNotificationDisableGPU());
myNotificationShown = true;
}
ApplicationManager.getApplication().executeOnPooledThread(() -> CefApp.getInstance().dispose());
}
}
}
/**
* Used to force JCEF scale in IDE-managed HiDPI mode.
*/
public static double getForceDeviceScaleFactor() {
return JreHiDpiUtil.isJreHiDPIEnabled() ? -1 : ScaleContext.create().getScale(DerivedScaleType.PIX_SCALE);
}
/**
* Returns normal (unscaled) size of the provided scaled size if IDE-managed HiDPI mode is enabled.
* In JRE-managed HiDPI mode, the method has no effect.
* <p>
* This method should be applied to size values (for instance, font size) previously scaled (explicitly or implicitly)
* via {@link com.intellij.ui.scale.JBUIScale#scale(int)}, before the values are used in HTML (in CSS, for instance).
*
* @see com.intellij.ui.scale.ScaleType
*/
public static int normalizeScaledSize(int scaledSize) {
return JreHiDpiUtil.isJreHiDPIEnabled() ? scaledSize : ROUND.round(scaledSize / getForceDeviceScaleFactor());
}
static boolean isRemoteEnabled() {
return IS_REMOTE_ENABLED;
}
static int readDebugPortFile(Path filePath) throws IOException {
try (Stream<String> lines = Files.lines(filePath)) {
String portNumber = lines.findFirst().orElseThrow(() -> {
return new IllegalArgumentException("Failed to read JCEF debugging port number in " + filePath);
});
int value = Integer.parseInt(portNumber);
if (value > 0) {
return value;
}
throw new IllegalDataException("Invalid JCEF JCEF debugging port number value: " + value);
}
}
@Nullable
CefDelegate getDelegate() {
return myDelegate;
}
private static @Nullable CefDelegate getActiveDelegate() {
return CefDelegate.EP.findFirstSafe(CefDelegate::isActive);
}
private static boolean isLinuxLibcSupported() {
String libcVersionString;
try {
libcVersionString = LibC.INSTANCE.gnu_get_libc_version();
} catch (UnsatisfiedLinkError e) {
LOG.warn("Failed load libc to check the version: " + e.getMessage());
return false;
}
Version version = Version.parseVersion(libcVersionString);
if (version == null) {
LOG.error("Failed to parse libc version: " + libcVersionString);
return false;
}
if (version.lessThan(2, 28)) {
LOG.warn("Incompatible glibc version: " + libcVersionString);
return false;
}
return true;
}
}