java/com/jetbrains/cef/remote/ServerStarter.java (337 lines of code) (raw):

package com.jetbrains.cef.remote; import org.cef.CefSettings; import org.cef.OS; import org.cef.callback.CefSchemeRegistrar; import org.cef.handler.CefAppHandler; import org.cef.misc.CefLog; import org.cef.misc.Utils; import java.io.File; import java.io.FileOutputStream; import java.io.IOException; import java.io.PrintStream; import java.nio.file.Path; import java.text.SimpleDateFormat; import java.util.*; import java.util.function.BiConsumer; import java.util.function.Consumer; public class ServerStarter { private static final boolean KILL_SERVER_ON_SHUTDOWN = Utils.getBoolean("JCEF_KILL_SERVER_ON_SHUTDOWN"); private static final boolean DISABLE_GPU = Utils.getBoolean("JCEF_DISABLE_GPU"); private static final boolean TRACE = Utils.getBoolean("JCEF_TRACE_SERVERSTARTER"); private static final boolean USE_PARAMS_FILE = Utils.getBoolean("JCEF_USE_PARAMS_FILE"); private static final boolean USE_SHORT_CMDLINE_PREFIXES = Utils.getBoolean("JCEF_USE_SHORT_CMDLINE_PREFIXES", true); private static final int WAIT_START_LOOP_SLEEP_MS = Utils.getInteger("JCEF_WAIT_START_LOOP_SLEEP_MS", 200); private static final String SPACE = "_SPACESYMBOL_"; static Map<String, Process> ourNativeServerProcesses = new HashMap<>(); static { if (KILL_SERVER_ON_SHUTDOWN) { CefLog.Debug("All cef_server instances will be killed at JVM exit."); Thread task = new Thread(() -> { for (String servTransport: ourNativeServerProcesses.keySet()) { Process p = ourNativeServerProcesses.get(servTransport); if (p != null) { p.destroyForcibly(); CefLog.Debug("Killed cef_server process [%s].", servTransport); } } }); Runtime.getRuntime().addShutdownHook(task); } } // Should be called in bg thread public static boolean startProcessAndWait(ThriftTransport thriftServer, CefAppHandler appHandler, String[] args, CefSettings settings, boolean deleteRootDir, long timeoutMs) { return startProcessAndWait(NativeServerManager.getServerExe(), thriftServer, appHandler, args, settings, deleteRootDir, null, timeoutMs); } // Should be called in bg thread public static boolean startProcessAndWait(File serverExe, ThriftTransport thriftServer, CefAppHandler appHandler, String[] args, CefSettings settings, boolean deleteRootDir, Map<String, String> env, long timeoutMs) { // Select log path String serverLogPath = Utils.getString("CEF_SERVER_LOG_PATH"); if (serverLogPath == null || serverLogPath.trim().isEmpty()) serverLogPath = CefLog.GetFilePath(); // Select log level String serverLogLevel = Utils.getString("CEF_SERVER_LOG_LEVEL"); if (serverLogLevel == null) serverLogLevel = NativeServerManager.ServerLogLevel.cef2native_str(CefLog.GetLogLevel()); else { try { final int nLogLevel = Integer.parseInt(serverLogLevel); serverLogLevel = NativeServerManager.ServerLogLevel.nativeDesc(nLogLevel); } catch (NumberFormatException e) { } } return startProcessAndWait(serverExe, thriftServer, appHandler, args, settings, serverLogPath, serverLogLevel, deleteRootDir, env, timeoutMs); } // Should be called in bg thread public static boolean startProcessAndWait(File serverExe, ThriftTransport thriftServer, CefAppHandler appHandler, String[] args, CefSettings settings, String logPath, String logLevel, boolean deleteRootDir, Map<String, String> env, long timeoutMs) { if (serverExe == null) serverExe = NativeServerManager.getServerExe(); if (serverExe == null) { CefLog.Error("Can't start native cef_server, file is null."); return false; } if (!serverExe.exists()) { CefLog.Error("Can't start native cef_server, file doesn't exist: %s", serverExe.getAbsolutePath()); return false; } Integer exitVal = startAndWaitImpl(serverExe, thriftServer, appHandler, args, settings, logPath, logLevel, deleteRootDir, env, timeoutMs); if (exitVal != null) { if (exitVal == 101) { // CefInitialize returns false. Probably, JCEF cache dir is locked. final SimpleDateFormat f = new SimpleDateFormat("hh_mm_ss_SSS"); final String newCacheDir = Path.of(System.getProperty("java.io.tmpdir")).resolve("cef_cache_" + thriftServer.toStringShort() + "_" + f.format(new Date())).toString(); CefLog.Info("Try to restart cef_server with another cache_dir '%s'.", newCacheDir); if (settings == null) settings = new CefSettings(); settings.cache_path = newCacheDir; exitVal = startAndWaitImpl(serverExe, thriftServer, appHandler, args, settings, logPath, logLevel, true, env, timeoutMs); } } return exitVal == null; } private static void processArg(String arg, Consumer<String> visitor) { if (TRACE) CefLog.Debug("processArg: " + arg); boolean skip = arg.startsWith("--browser-subprocess-path=") || arg.startsWith("--main-bundle-path=") || arg.startsWith("--framework-dir-path="); if (skip) CefLog.Debug("Skip cmdline swintch '%s'", arg); else visitor.accept(arg); } private static void processSetting(String name, String value, BiConsumer<String, String> visitor) { if (TRACE) CefLog.Debug("processSetting: " + name + "=" + value); boolean skip = "browser_subprocess_path".equals(name) || "resources_dir_path".equals(name) || "locales_dir_path".equals(name); if (skip) CefLog.Debug("Skip setting %s=%s", name, value); else visitor.accept(name, value); } private static void processCustomSchemes(CefAppHandler appHandler, BiConsumer<String, Integer> visitor) { if (appHandler != null) { CefSchemeRegistrar collector = new CefSchemeRegistrar() { @Override public boolean addCustomScheme(String schemeName, boolean isStandard, boolean isLocal, boolean isDisplayIsolated, boolean isSecure, boolean isCorsEnabled, boolean isCspBypassing, boolean isFetchEnabled) { int options = 0; if (isStandard) options |= 1 << 0; if (isLocal) options |= 1 << 1; if (isDisplayIsolated) options |= 1 << 2; if (isSecure) options |= 1 << 3; if (isCorsEnabled) options |= 1 << 4; if (isCspBypassing) options |= 1 << 5; if (isFetchEnabled) options |= 1 << 6; if (TRACE) CefLog.Debug("process CefCustomScheme: " + schemeName + "=" + options); visitor.accept(schemeName, options); return false; } }; appHandler.onRegisterCustomSchemes(collector); } } private static void addSwitchesDisableGPU(Consumer<String> visitor) { if (DISABLE_GPU) { CefLog.Debug("Add disable GPU chromium switches."); visitor.accept("--disable-gpu"); visitor.accept("--disable-gpu-compositing"); visitor.accept("--disable-gpu-vsync"); visitor.accept("--disable-software-rasterizer"); visitor.accept("--disable-extensions"); } } private static boolean isPrefixedArg(String arg) { final String[] ourPrefixes = new String[] { "arg:", "cmd_switch:" }; for (String prefix: ourPrefixes) if (arg.startsWith(prefix)) return true; return false; } private static boolean isPrefixedCS(String arg) { final String[] ourPrefixes = new String[] { "sch:", "customscheme:" }; for (String prefix: ourPrefixes) if (arg.startsWith(prefix)) return true; return false; } private static boolean isPrefixedSetting(String arg) { final String[] ourPrefixes = new String[] { "cs:", "cef_setting:" }; for (String prefix: ourPrefixes) if (arg.startsWith(prefix)) return true; return false; } private static void prepareStartParamsWithCmdLine(ProcessBuilder builder, File serverExe, CefAppHandler appHandler, String[] args, CefSettings settings) { // Use prefixed command line switches, CefSettings and custom schemes (from CefAppHandler). // 1. command line args List<String> prefixedArgs = new ArrayList<>(); List<String> prefixedSettings = new ArrayList<>(); List<String> prefixedCS = new ArrayList<>(); String prefix = USE_SHORT_CMDLINE_PREFIXES ? "arg:" : "cmd_switch:"; if (args != null && args.length > 0) for (String arg: args) { if (isPrefixedArg(arg)) { prefixedArgs.add(arg); continue; } if (isPrefixedCS(arg)) { prefixedCS.add(arg); continue; } if (isPrefixedSetting(arg)) { prefixedSettings.add(arg); continue; } String finalPrefix = prefix; processArg(arg, s -> prefixedArgs.add(finalPrefix + s.replace(" ", SPACE))); } if (DISABLE_GPU) { String finalPrefix = prefix; addSwitchesDisableGPU(s -> prefixedArgs.add(finalPrefix + s)); } // 2. settings prefix = USE_SHORT_CMDLINE_PREFIXES ? "cs:" : "cef_setting:"; if (settings != null) { Map<String, String> settingsMap = settings.toMap(); for (Map.Entry<String, String> entry : settingsMap.entrySet()) { String finalPrefix = prefix; processSetting(entry.getKey(), entry.getValue(), (n, v) -> prefixedSettings.add(finalPrefix + n + "=" + v.replace(" ", SPACE))); } } if (OS.isMacintosh() && serverExe != null) { File subprocess = new File(serverExe.getParentFile().getParentFile(), "Frameworks/cef_server Helper.app/Contents/MacOS/cef_server Helper"); if (settings != null && settings.browser_subprocess_path != null) CefLog.Debug("browser_subprocess_path setting '%s' will be overridden with bundled path '%s'", settings.browser_subprocess_path, subprocess.getAbsolutePath()); prefixedSettings.add(prefix + "browser_subprocess_path=" + subprocess.getAbsolutePath().replace(" ", SPACE)); } // 3. custom schemes String finalPrefix = USE_SHORT_CMDLINE_PREFIXES ? "sch:" : "customscheme:"; processCustomSchemes(appHandler, (n,o) -> prefixedCS.add(finalPrefix + n + "=" + o)); // 4. add results to builder's command line args prefixedArgs.forEach(builder.command()::add); prefixedSettings.forEach(builder.command()::add); prefixedCS.forEach(builder.command()::add); } private static boolean prepareStartParamsWithFile(ProcessBuilder builder, File serverExe, CefAppHandler appHandler, String[] args, CefSettings settings) { // Write chromium command line switches, CefSettings and custom schemes to the params file. final long t0 = System.nanoTime(); final Path settingsFileName = Path.of(System.getProperty("java.io.tmpdir")).resolve("cef_server_params.txt"); File f = new File(settingsFileName.toString()); PrintStream ps; try { new FileOutputStream(f).close(); // delete the content of the file f.createNewFile(); ps = new PrintStream(new FileOutputStream(f, false)); } catch (IOException e) { CefLog.Error("Can't create temp file with server params path=%s, msg=%s", settingsFileName.toString(), e.getMessage()); return false; } // 1. command line args final String sectionCmdLine = "[COMMAND_LINE]:"; ps.printf("%s\n", sectionCmdLine); if (args != null && args.length > 0) for (String arg: args) processArg(arg, s -> ps.printf("%s\n", s)); if (DISABLE_GPU) addSwitchesDisableGPU(s -> ps.println(s)); // 2. settings ps.printf("[SETTINGS]:\n"); if (settings != null) { Map<String, String> settingsMap = settings.toMap(); for (Map.Entry<String, String> entry : settingsMap.entrySet()) processSetting(entry.getKey(), entry.getValue(), (n,v) -> ps.printf("%s=%s\n", n, v)); } if (OS.isMacintosh() && serverExe != null) { File subprocess = new File(serverExe.getParentFile().getParentFile(), "Frameworks/cef_server Helper.app/Contents/MacOS/cef_server Helper"); if (settings != null && settings.browser_subprocess_path != null) CefLog.Debug("browser_subprocess_path setting '%s' will be overridden with bundled path '%s'", settings.browser_subprocess_path, subprocess.getAbsolutePath()); ps.printf("browser_subprocess_path=%s\n", subprocess.getAbsolutePath()); } // 3. custom schemes ps.printf("[CUSTOM_SCHEMES]:\n"); processCustomSchemes(appHandler, (n,o) -> ps.printf("%s|%d\n", n, o)); ps.flush(); ps.close(); CefLog.Debug("Settings were written to file, spent %d mcs", (System.nanoTime() - t0)/1000); builder.command().add(String.format("--params=%s", f.getAbsolutePath())); return true; } // Returns: // null when the process has been started successfully // Integer.MIN_VALUE when can't start process because of IO-errors // exit code, otherwise private static Integer startAndWaitImpl(File serverExe, ThriftTransport thriftServer, CefAppHandler appHandler, String[] args, CefSettings settings, String logPath, String logLevel, boolean deleteRootDir, Map<String, String> env, long timeoutMs) { final long t0 = System.nanoTime(); CefLog.Info("Start native cef_server with cache path: %s", settings == null ? null : settings.cache_path); CefLog.Debug("cef_server executable path='%s'", serverExe.getAbsolutePath()); if (ourNativeServerProcesses.get(thriftServer.toString()) != null) CefLog.Debug("Handle of server process will be overwritten."); ourNativeServerProcesses.remove(thriftServer.toString()); ProcessBuilder builder = new ProcessBuilder(serverExe.getAbsolutePath()); CefLog.Debug("\tWorking dir %s", serverExe.getParentFile()); builder.directory(serverExe.getParentFile()); builder.redirectOutput(ProcessBuilder.Redirect.INHERIT); builder.redirectError(ProcessBuilder.Redirect.INHERIT); if (env != null && env.size() > 0) { builder.environment().putAll(env); if (CefLog.IsDebugEnabled()) { String logTxt = ""; for (Map.Entry<String, String> entry : env.entrySet()) { if (logTxt.length() > 0) logTxt += "; "; logTxt += entry.getKey() + "=" + entry.getValue(); } CefLog.Debug("\tEnvironment vars: %s", logTxt); } } if (thriftServer.isTcp()) { CefLog.Debug("\tUse tcp-port %d", thriftServer.getPort()); builder.command().add(String.format("--port=%d", thriftServer.getPort())); } else { CefLog.Debug("\tUse pipe %s", thriftServer.getPipe()); builder.command().add(String.format("--pipe=%s", thriftServer.getPipe())); } String logStream = "stderr"; if (logPath != null && !logPath.isEmpty()) { logStream = "file '" + logPath + "'"; builder.command().add(String.format("--logfile=%s", logPath.trim())); } CefLog.Info("Native server logging: level '%s', stream: '%s'", logLevel, logStream); builder.command().add(String.format("--loglevel=%s", logLevel)); if (System.getenv().containsKey("DEBUG_CEF_SERVER")) builder.command().add("--cef-server-wait-debugger"); if (deleteRootDir) builder.command().add("--deleteRootCacheDir"); if (!USE_PARAMS_FILE || !prepareStartParamsWithFile(builder, serverExe, appHandler, args, settings)) { prepareStartParamsWithCmdLine(builder, serverExe, appHandler, args, settings); } Process p; try { p = builder.start(); ourNativeServerProcesses.put(thriftServer.toString(), p); } catch (Throwable e) { CefLog.Error("Can't start native cef_server, exception: %s", e.getMessage()); return Integer.MIN_VALUE; } // Wait for native server Integer exitVal = null; boolean running = false; final long t1 = System.nanoTime(); do { try { Thread.sleep(WAIT_START_LOOP_SLEEP_MS); } catch (InterruptedException e) { CefLog.Error("Exception during waiting for native cef_server: %s", e.getMessage()); } CefLog.Debug("Waiting for server %s starting...", thriftServer.toStringShort()); // 1. Check process exit values. try { exitVal = p.exitValue(); } catch (IllegalThreadStateException e) {} if (exitVal != null) { CefLog.Error("Native cef_server exited with code %d", exitVal); if (exitVal == 100) { CefLog.Error("It means that cef_server can't load CEF framework library."); } else if (exitVal == 101) { CefLog.Error("It means that CefInitialize returns false - probably, JCEF cache dir is locked."); // TODO: search stdout for string 'Opening in existing browser session' } ourNativeServerProcesses.remove(thriftServer.toString()); return exitVal; } // 2. Try to connect with cef_server. running = NativeServerManager.isRunning(thriftServer) != null; } while (!running && (System.nanoTime() - t1 < timeoutMs*1000000)); // Check whether the server is running or not. if (!running && !(running = (NativeServerManager.isRunning(thriftServer, true) != null))) { if (p.isAlive()) CefLog.Error("Native cef_server was started but client can't connect."); else { CefLog.Error("Can't start native cef_server, process is dead."); ourNativeServerProcesses.remove(thriftServer.toString()); } try { exitVal = p.exitValue(); } catch (IllegalThreadStateException e) {} } else CefLog.Debug("Server is started. Spent ms: process starting %d, waiting %d", (t1 - t0)/1000000, (System.nanoTime() - t1)/1000000); return running ? null : exitVal; } }