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;
}
}