java/com/jetbrains/cef/remote/NativeServerManager.java (397 lines of code) (raw):

package com.jetbrains.cef.remote; import com.jetbrains.cef.remote.thrift.transport.TSocket; import com.jetbrains.cef.remote.thrift.transport.TTransportException; import org.cef.CefSettings; import org.cef.OS; import org.cef.misc.CefLog; import org.cef.misc.Utils; import java.io.*; import java.net.InetAddress; import java.net.ServerSocket; import java.net.StandardProtocolFamily; import java.net.UnixDomainSocketAddress; import java.nio.channels.SocketChannel; import java.nio.file.InvalidPathException; import java.nio.file.Path; import java.util.*; import java.util.function.BooleanSupplier; public class NativeServerManager { public static final String ALT_CEF_SERVER_PATH = Utils.getString("ALT_CEF_SERVER_PATH"); private static final int WAIT_LOOP_SLEEP_MS = Utils.getInteger("JCEF_WAIT_LOOP_SLEEP_MS", 200); public static boolean isProcessAlive(ThriftTransport thriftServer) { Process p = ServerStarter.ourNativeServerProcesses.get(thriftServer.toString()); return p != null && p.isAlive(); } public static boolean isConnectable(int port) { return isConnectable(new ThriftTransport(port), false); } public static boolean isConnectable(ThriftTransport thriftServer) { return isConnectable(thriftServer, false); } private static boolean isConnectable(ThriftTransport thriftServer, boolean withDebug) { try { if (thriftServer.isTcp()) { try { TSocket socket = new TSocket("localhost", thriftServer.getPort()); socket.open(); socket.close(); if (withDebug) CefLog.Debug("isConnectable: tcp-port %d, opened and connected.", thriftServer.getPort()); return true; } catch (TTransportException e) { if (withDebug) CefLog.Debug("isConnectable: tcp-port %d, TTransportException occurred: %s", thriftServer.getPort(), e.getMessage()); } return false; } try { if (OS.isWindows()) { WindowsPipeSocket pipe = new WindowsPipeSocket(thriftServer.getPipe()); pipe.close(); if (withDebug) CefLog.Debug("isConnectable: win-pipe '%s', opened and connected.", thriftServer.getPipe()); return true; } UnixDomainSocketAddress socketAddress = UnixDomainSocketAddress.of(thriftServer.getPipe()); SocketChannel channel = SocketChannel.open(StandardProtocolFamily.UNIX); channel.connect(socketAddress); channel.close(); if (withDebug) CefLog.Debug("isConnectable: pipe '%s', opened and connected.", thriftServer.getPipe()); return true; } catch (IOException e) { if (withDebug) CefLog.Debug("isConnectable: pipe '%s', IOException occurred: %s", thriftServer.getPipe(), e.getMessage()); } } catch (Throwable e) { CefLog.Error("isConnectable: exception %s", e.getMessage()); } return false; } private static boolean isServerSocketBusy(int port, boolean withDebug) { try { ServerSocket serverSocket = null; try { serverSocket = new ServerSocket(port, 1, InetAddress.getByName("localhost")); } catch (Throwable e) { if (withDebug) CefLog.Debug("isServerSocketBusy: can't open tcp-port %d, exception occurred: %s", port, e.getMessage()); return true; } if (withDebug) CefLog.Debug("isServerSocketBusy: tcp-port %d, successfully opened.", port); serverSocket.close(); } catch (Throwable e) { CefLog.Error("isServerSocketBusy: exception %s", e.getMessage()); } return false; } public static String isRunning(ThriftTransport thriftServer) { return isRunning(thriftServer, false); } // returns root_cache_path of running server (or null if not running) public static String isRunning(ThriftTransport transport, boolean withDebug) { if (ServerStarter.ourNativeServerProcesses.get(transport.toString()) != null && !ServerStarter.ourNativeServerProcesses.get(transport.toString()).isAlive()) { if (withDebug) CefLog.Debug("isRunning: server process is not alive."); return null; } try { if (transport.isTcp()) { // At first, we check whether the server socket is busy. if (!isServerSocketBusy(transport.getPort(), withDebug)) return null; // Well, socket is busy and server seems to be running. Let's try to connect to it. } if (!isConnectable(transport, withDebug)) return null; // Successfully connected to server transport => server seems to be running. Let's connect and check an echo. RpcExecutor test; try { test = new RpcExecutor().openTransport(transport); } catch (TTransportException e) { if (withDebug) CefLog.Debug("isRunning: TTransportException occurred when open server transport: %s", e.getMessage()); return null; } String testMsg = "test_message786"; String echoMsg = test.execObj(s -> s.echo(testMsg)); String root = null; final boolean isEchoCorrect = echoMsg != null && echoMsg.equals(testMsg); if (!isEchoCorrect) CefLog.Error("isRunning: cef_server seems to be running, but echo is incorrect: '%s' (original '%s')", echoMsg, testMsg); else { root = test.execObj(s -> s.getServerInfo("root")); if (withDebug) CefLog.Debug("isRunning: cef_server is running and echo is correct, root='%s'", root); } test.closeTransport(); return isEchoCorrect ? root : null; } catch (Throwable e) { CefLog.Error("isRunning: exception %s", e.getMessage()); } return null; } public static String getServerState(ThriftTransport thriftServer) { try { RpcExecutor test = new RpcExecutor().openTransport(thriftServer); String state = test.execObj(s -> s.getServerInfo("state")); test.closeTransport(); return state; } catch (TTransportException e) { return "stopped"; } } // returns true when server was stopped successfully public static boolean stopAndWait(ThriftTransport thriftServer, long timeoutMs) { CefLog.Debug("Stop running cef_server instance."); try { RpcExecutor test = new RpcExecutor().openTransport(thriftServer); String state = test.execObj(s -> s.getServerInfo("state")); CefLog.Debug("Server state before stop: %s", state); test.exec(s -> s.stop()); test.closeTransport(); } catch (TTransportException e) { CefLog.Debug("Exception when trying to stop server, err: %s", e.getMessage()); } // Wait for stopping boolean stopped = waitForStopped(thriftServer, timeoutMs); if (!stopped) { CefLog.Error("Can't stop server in %d ms (process is %s)", timeoutMs, isProcessAlive(thriftServer) ? "alive" : "dead"); CefLog.Debug("Server state: %s", getServerState(thriftServer)); return false; } ServerStarter.ourNativeServerProcesses.remove(thriftServer.toString()); return true; } private static boolean isDefaultRoot(String rootPath) { if (OS.isWindows()) return rootPath.compareToIgnoreCase("~\\AppData\\Local\\CEF\\User Data") == 0; if (OS.isLinux()) return rootPath.compareToIgnoreCase("~/.config/cef_user_data") == 0; return rootPath.compareToIgnoreCase("~/Library/Application Support/CEF/User Data") == 0; } public static boolean fixRootInSettings(CefSettings settings, String newRootDirName) { try { return fixRootInSettingsImpl(settings, newRootDirName); } catch (Throwable e) { CefLog.Error("Can't fix root_cache_path in settings: %s", e.getMessage()); } return false; } private static boolean fixRootInSettingsImpl(CefSettings settings, String newRootDirName) { List<String> runningInstancesRoots = ProcessLister.findRunningInstancesRoots(); if (runningInstancesRoots == null || runningInstancesRoots.isEmpty()) return false; if (settings.cache_path != null && !settings.cache_path.isEmpty()) { Path settingsRoot; try { settingsRoot = Path.of(settings.cache_path); } catch (InvalidPathException e) { CefLog.Error("Can't find path '%s': %s", settings.cache_path, e.getMessage()); return false; } for (String sr : runningInstancesRoots) { Path r; try { r = Path.of(sr); } catch (InvalidPathException e) { CefLog.Error("Can't find path '%s': %s", sr, e.getMessage()); continue; } if (r.equals(settingsRoot)) { settings.cache_path = Path.of(System.getProperty("java.io.tmpdir")).resolve(newRootDirName).toString(); CefLog.Info("Non-empty settings.cache_path='%s' conflicts with existing root_cache_path, will be replaced with '%s'.", r, settings.cache_path); return true; } } } else { // settings.cache_path == null for (String sr: runningInstancesRoots) { if (NativeServerManager.isDefaultRoot(sr)) { settings.cache_path = Path.of(System.getProperty("java.io.tmpdir")).resolve(newRootDirName).toString(); CefLog.Info("Empty settings.cache_path will be replaced with '%s' (because found CEF instance with system-default root_cache_path '%s')", settings.cache_path, sr); return true; } } } return false; } public static boolean waitForRunning(ThriftTransport thriftServer, long timeoutMs) { return waitFor(() -> isRunning(thriftServer) != null, timeoutMs, thriftServer.toStringShort() + " starting"); } public static boolean waitForStopped(ThriftTransport thriftServer, long timeoutMs) { return waitFor(() -> isRunning(thriftServer) == null, timeoutMs, thriftServer.toStringShort() + " stopping"); } private static boolean waitFor(BooleanSupplier checker, long timeoutMs, String hint) { final long startNs = System.nanoTime(); boolean success; do { try { Thread.sleep(WAIT_LOOP_SLEEP_MS); } catch (InterruptedException e) { throw new RuntimeException(e); } CefLog.Debug("Waiting for server %s", hint); success = checker.getAsBoolean(); } while (!success && (System.nanoTime() - startNs < timeoutMs*1000000)); return success; } public static boolean isRemoteSupported() { File cef_server_exe = getServerExe(); if (cef_server_exe == null) { // NOTE: cef_server can be manually started on custom port (for example, in debugging) return ThriftTransport.MANUAL_SERVER_SELECT || isRunning(ThriftTransport.ourDefaultServer) != null; } return cef_server_exe.exists() && !cef_server_exe.isDirectory(); } static File getServerExe() { if (ALT_CEF_SERVER_PATH != null && !ALT_CEF_SERVER_PATH.trim().isEmpty()) return new File(ALT_CEF_SERVER_PATH); ProcessHandle.Info i = ProcessHandle.current().info(); String cmd = i.command().get(); if (cmd == null || cmd.isEmpty()) { CefLog.Warn("Can't determine cef_server location via ProcessHandle (because the command is empty)."); return findExeViaSystemProperty(); } final boolean isJava = OS.isWindows() ? cmd.endsWith("java.exe") : cmd.endsWith("java"); if (isJava) { File javabin = new File(cmd); if (!javabin.exists() || javabin.isDirectory()) { CefLog.Warn("Can't determine cef_server location via ProcessHandle (because calculated java.exe doesn't exist), cmd=%s"); return findExeViaSystemProperty(); } File result; if (OS.isMacintosh()) result = new File(javabin.getParentFile().getParentFile().getParentFile(), "Frameworks/cef_server.app/Contents/MacOS/cef_server"); else if (OS.isLinux()) result = new File(javabin.getParentFile().getParentFile(), "lib/cef_server"); else result = new File(javabin.getParentFile(), "cef_server.exe"); if (!result.exists()) { CefLog.Warn("Can't determine cef_server location via java-process path '%s' (because calculated path '%s' doesn't exist), cmd=%s", javabin.getAbsolutePath(), result.getAbsolutePath(), cmd); return findExeViaSystemProperty(); } return result; } // // It seems that JVM is started via the native launcher. // File result = findExeViaSystemProperty(); if (result != null) { CefLog.Debug("Java is started via native launcher. Found cef_server path %s (via system propety)", result.getAbsolutePath()); return result; } // TODO: get path of loaded libjvm and calculate relative server path File launcher = new File(cmd); if (!launcher.exists()) { CefLog.Warn("Can't find cef_server in bundled jbr (launcher '%s' doesn't exist), cmd=%s", launcher.getAbsolutePath(), cmd); return null; } if (OS.isMacintosh()) result = new File(launcher.getParentFile().getParentFile(), "jbr/Contents/Frameworks/cef_server.app/Contents/MacOS/cef_server"); else if (OS.isLinux()) result = new File(launcher.getParentFile().getParentFile(), "jbr/lib/cef_server"); else result = new File(new File(new File(launcher.getParentFile().getParentFile(), "jbr"), "bin"), "cef_server.exe"); if (!result.exists()) { CefLog.Warn("Can't find cef_server in bundled jbr (calculated path '%s' doesn't exist), cmd=%s", result.getAbsolutePath(), cmd); return null; } CefLog.Debug("Java is started via native launcher. Found cef_server path %s (in bundled jbr)", result.getAbsolutePath()); return result; } private static File findExeViaSystemProperty() { String javaPath = System.getProperty("java.home"); if (javaPath == null || javaPath.isEmpty()) { CefLog.Error("Can't find cef_server binary: system property 'java.home' is empty."); return null; } File javaDir = new File(javaPath); if (!javaDir.exists() || !javaDir.isDirectory()) { CefLog.Error("Can't find cef_server binary via System.getProperty('java.home'): java directory doesn't exist, 'java.home'=%s", javaPath); return null; } File result; if (OS.isMacintosh()) // javaPath points to Home: /Applications/IntelliJ IDEA Ultimate 2024.3 Nightly.app/Contents/jbr/Contents/Home result = new File(javaDir.getParentFile(), "Frameworks/cef_server.app/Contents/MacOS/cef_server"); else if (OS.isLinux()) result = new File(javaDir, "lib/cef_server"); else result = new File(new File(javaDir, "bin"), "cef_server.exe"); if (!result.exists()) { CefLog.Debug("Can't find cef_server binary via System.getProperty('java.home'): file %s doesn't exist, 'java.home'=%s", result.getAbsolutePath(), javaPath); return null; } CefLog.Debug("Found cef_server binary '%s' via System.getProperty('java.home')=%s", result.getAbsolutePath(), javaPath); return result; } public static class ServerLogLevel { public final static int LEVEL_DISABLED = 100; public final static int LEVEL_FATAL = 10; public final static int LEVEL_ERROR = 9; public final static int LEVEL_WARN = 8; public final static int LEVEL_INFO = 7; public final static int LEVEL_DEBUG = 6; public final static int LEVEL_TRACE = 5; public static int cef2native(CefSettings.LogSeverity severity) { if (severity == CefSettings.LogSeverity.LOGSEVERITY_DISABLE) return LEVEL_DISABLED; else if (severity == CefSettings.LogSeverity.LOGSEVERITY_DEFAULT) return LEVEL_INFO; else if (severity == CefSettings.LogSeverity.LOGSEVERITY_FATAL) return LEVEL_FATAL; else if (severity == CefSettings.LogSeverity.LOGSEVERITY_ERROR) return LEVEL_ERROR; else if (severity == CefSettings.LogSeverity.LOGSEVERITY_WARNING) return LEVEL_WARN; else if (severity == CefSettings.LogSeverity.LOGSEVERITY_INFO) return LEVEL_DEBUG; else if (severity == CefSettings.LogSeverity.LOGSEVERITY_VERBOSE) return LEVEL_TRACE; return LEVEL_DISABLED; } public static String nativeDesc(int level) { if (level == LEVEL_DISABLED) return "disabled"; if (level == LEVEL_FATAL) return "fatal"; if (level == LEVEL_ERROR) return "error"; if (level == LEVEL_WARN) return "warn"; if (level == LEVEL_INFO) return "info"; if (level == LEVEL_DEBUG) return "debug"; if (level == LEVEL_TRACE) return "trace"; return "unknown_logging_level_" + level; } static String cef2native_str(CefSettings.LogSeverity severity) { if (severity == CefSettings.LogSeverity.LOGSEVERITY_DISABLE) return "disable"; else if (severity == CefSettings.LogSeverity.LOGSEVERITY_DEFAULT) return "info"; else if (severity == CefSettings.LogSeverity.LOGSEVERITY_FATAL) return "fatal"; else if (severity == CefSettings.LogSeverity.LOGSEVERITY_ERROR) return "err"; else if (severity == CefSettings.LogSeverity.LOGSEVERITY_WARNING) return "warn"; else if (severity == CefSettings.LogSeverity.LOGSEVERITY_INFO) return "debug"; else if (severity == CefSettings.LogSeverity.LOGSEVERITY_VERBOSE) return "verb"; return "disable"; } public static int str2native(String level) { if (level == null || level.isEmpty()) return LEVEL_DISABLED; level = level.toLowerCase(); if (level.contains("disable")) return LEVEL_DISABLED; if (level.contains("fatal")) return LEVEL_FATAL; if (level.contains("err")) return LEVEL_ERROR; if (level.contains("warn")) return LEVEL_WARN; if (level.contains("info")) return LEVEL_INFO; if (level.contains("debug")) return LEVEL_DEBUG; if (level.contains("trace")) return LEVEL_TRACE; return LEVEL_INFO; } } }