/*
* Copyright 2014 TWO SIGMA OPEN SOURCE, LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.twosigma.beaker.core.rest;
import com.google.inject.Inject;
import com.google.inject.Singleton;
import com.sun.jersey.api.Responses;
import com.twosigma.beaker.core.module.config.BeakerConfig;
import com.twosigma.beaker.shared.module.config.WebServerConfig;
import com.twosigma.beaker.shared.module.util.GeneralUtils;
import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.File;
import java.io.InputStreamReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStreamWriter;
import java.io.PrintWriter;
import java.net.DatagramSocket;
import java.net.InetAddress;
import java.net.ServerSocket;
import java.net.URI;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.nio.file.attribute.PosixFilePermission;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.EnumSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import javax.ws.rs.DefaultValue;
import javax.ws.rs.GET;
import javax.ws.rs.Path;
import javax.ws.rs.PathParam;
import javax.ws.rs.Produces;
import javax.ws.rs.QueryParam;
import javax.ws.rs.WebApplicationException;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.Response;
import org.apache.commons.codec.binary.Base64;
import org.apache.commons.lang3.RandomStringUtils;
import org.apache.commons.lang3.StringUtils;
import org.apache.http.HttpStatus;
import org.apache.http.client.fluent.Request;
import org.jvnet.winp.WinProcess;
/**
* This is the service that locates a plugin service. And a service will be started if the target
* service doesn't exist. See {@link locatePluginService} for details
*/
@Path("plugin-services")
@Produces(MediaType.APPLICATION_JSON)
@Singleton
public class PluginServiceLocatorRest {
// these 3 times are in millis
private static final int RESTART_ENSURE_RETRY_MAX_WAIT = 30*1000;
private static final int RESTART_ENSURE_RETRY_INTERVAL = 10;
private static final int RESTART_ENSURE_RETRY_MAX_INTERVAL = 2500;
private static final String REST_RULES =
"location %(base_url)s/ {\n" +
" proxy_pass http://127.0.0.1:%(port)s/;\n" +
" proxy_set_header Authorization \"Basic %(auth)s\";\n" +
"}\n";
private static final String IPYTHON_RULES_BASE =
" rewrite ^%(base_url)s/(.*)$ /$1 break;\n" +
" proxy_pass http://127.0.0.1:%(port)s;\n" +
" proxy_http_version 1.1;\n" +
" proxy_set_header Upgrade $http_upgrade;\n" +
" proxy_set_header Connection \"upgrade\";\n" +
" proxy_set_header Host 127.0.0.1:%(port)s;\n" +
" proxy_set_header Origin \"http://127.0.0.1:%(port)s\";\n" +
"}\n" +
"location %(base_url)s/login {\n" +
" proxy_pass http://127.0.0.1:%(port)s/login;\n" +
"}\n";
private static final String IPYTHON1_RULES =
"location %(base_url)s/kernels/ {\n" +
" proxy_pass http://127.0.0.1:%(port)s/kernels;\n" +
"}\n" +
"location ~ %(base_url)s/kernels/[0-9a-f-]+/ {\n" +
IPYTHON_RULES_BASE;
private static final String IPYTHON2_RULES =
"location %(base_url)s/api/kernels/ {\n" +
" proxy_pass http://127.0.0.1:%(port)s/api/kernels;\n" +
"}\n" +
"location %(base_url)s/api/sessions/ {\n" +
" proxy_pass http://127.0.0.1:%(port)s/api/sessions;\n" +
"}\n" +
"location ~ %(base_url)s/api/kernels/[0-9a-f-]+/ {\n" +
IPYTHON_RULES_BASE;
private final String nginxDir;
private final String nginxBinDir;
private final String nginxStaticDir;
private final String nginxServDir;
private final String nginxExtraRules;
private final Map<String, String> nginxPluginRules;
private final String pluginDir;
private final String [] nginxCommand;
private final String [] nginxRestartCommand;
private String[] nginxEnv = null;
private final Boolean publicServer;
private final Integer portBase;
private final Integer servPort;
private final Integer corePort;
private final Integer restartPort;
private final Integer reservedPortCount;
private final String authCookie;
private final Map<String, String> pluginLocations;
private final Map<String, List<String>> pluginArgs;
private final Map<String, String[]> pluginEnvps;
private final OutputLogService outputLogService;
private final Base64 encoder;
private final String corePassword;
private final String urlHash;
private final String nginxTemplate;
private final String ipythonTemplate;
private final Map<String, PluginConfig> plugins = new HashMap<>();
private Process nginxProc;
private int portSearchStart;
@Inject
private PluginServiceLocatorRest(
BeakerConfig bkConfig,
WebServerConfig webServerConfig,
OutputLogService outputLogService,
GeneralUtils utils) throws IOException {
this.nginxDir = bkConfig.getNginxDirectory();
this.nginxBinDir = bkConfig.getNginxBinDirectory();
this.nginxStaticDir = bkConfig.getNginxStaticDirectory();
this.nginxServDir = bkConfig.getNginxServDirectory();
this.nginxExtraRules = bkConfig.getNginxExtraRules();
this.nginxPluginRules = bkConfig.getNginxPluginRules();
this.pluginDir = bkConfig.getPluginDirectory();
this.publicServer = bkConfig.getPublicServer();
this.portBase = bkConfig.getPortBase();
this.servPort = this.portBase + 1;
this.corePort = this.portBase + 2;
this.restartPort = this.portBase + 3;
this.reservedPortCount = bkConfig.getReservedPortCount();
this.authCookie = bkConfig.getAuthCookie();
this.pluginLocations = bkConfig.getPluginLocations();
this.pluginEnvps = bkConfig.getPluginEnvps();
this.urlHash = bkConfig.getHash();
this.pluginArgs = new HashMap<>();
this.outputLogService = outputLogService;
this.encoder = new Base64();
this.nginxTemplate = utils.readFile(this.nginxDir + "/nginx.conf.template");
if (nginxTemplate == null) {
throw new RuntimeException("Cannot get nginx template");
}
this.ipythonTemplate = ("c = get_config()\n" +
"c.NotebookApp.ip = u'127.0.0.1'\n" +
"c.NotebookApp.port = %(port)s\n" +
"c.NotebookApp.open_browser = False\n" +
"c.NotebookApp.password = u'%(hash)s'\n");
this.nginxCommand = new String[7];
this.nginxCommand[0] = this.nginxBinDir + (this.nginxBinDir.isEmpty() ? "nginx" : "/nginx");
this.nginxCommand[1] = "-p";
this.nginxCommand[2] = this.nginxServDir;
this.nginxCommand[3] = "-c";
this.nginxCommand[4] = this.nginxServDir + "/conf/nginx.conf";
this.nginxCommand[5] = "-g";
this.nginxCommand[6] = "error_log stderr;";
this.nginxRestartCommand = new String [9];
this.nginxRestartCommand[0] = this.nginxBinDir + (this.nginxBinDir.isEmpty() ? "nginx" : "/nginx");
this.nginxRestartCommand[1] = "-p";
this.nginxRestartCommand[2] = this.nginxServDir;
this.nginxRestartCommand[3] = "-c";
this.nginxRestartCommand[4] = this.nginxServDir + "/conf/nginx.conf";
this.nginxRestartCommand[5] = "-g";
this.nginxRestartCommand[6] = "error_log stderr;";
this.nginxRestartCommand[7] = "-s";
this.nginxRestartCommand[8] = "reload";
this.corePassword = webServerConfig.getPassword();
// record plugin options from cli and to pass through to individual plugins
for (Map.Entry<String, String> e: bkConfig.getPluginOptions().entrySet()) {
addPluginArgs(e.getKey(), e.getValue());
}
// Add shutdown hook
Runtime.getRuntime().addShutdownHook(new Thread() {
@Override
public void run() {
System.out.println("\nshutting down beaker");
shutdown();
System.out.println("done, exiting");
}
});
portSearchStart = this.portBase + this.reservedPortCount;
// on MacOS add library search path
if (macosx()) {
List<String> envList = new ArrayList<>();
for (Map.Entry<String, String> entry: System.getenv().entrySet()) {
envList.add(entry.getKey() + "=" + entry.getValue());
}
envList.add("DYLD_LIBRARY_PATH=./nginx/bin");
this.nginxEnv = new String[envList.size()];
envList.toArray(this.nginxEnv);
}
}
private String pythonBaseCommand(String pluginId, String command) {
// Should pass pluginArgs too XXX?
String base = this.pluginLocations.containsKey(pluginId) ?
this.pluginLocations.get(pluginId) : this.pluginDir;
base += "/" + command;
if (windows()) {
base = "python " + base;
}
return base;
}
private boolean macosx() {
return System.getProperty("os.name").contains("Mac");
}
private boolean windows() {
return System.getProperty("os.name").contains("Windows");
}
private static boolean windowsStatic() {
return System.getProperty("os.name").contains("Windows");
}
public void start() throws InterruptedException, IOException {
startReverseProxy();
}
private void startReverseProxy() throws InterruptedException, IOException {
generateNginxConfig();
System.out.println("starting nginx instance (" + this.nginxDir +")");
Process proc = Runtime.getRuntime().exec(this.nginxCommand, this.nginxEnv);
startGobblers(proc, "nginx", null, null);
this.nginxProc = proc;
}
private void shutdown() {
StreamGobbler.shuttingDown();
if (windows()) {
new WinProcess(this.nginxProc).killRecursively();
} else {
this.nginxProc.destroy(); // send SIGTERM
}
for (PluginConfig p : this.plugins.values()) {
p.shutDown();
}
}
private boolean internalEnvar(String var) {
String [] vars = {"beaker_plugin_password",
"beaker_tmp_dir",
"beaker_core_password"};
for (int i = 0; i < vars.length; i++)
if (var.startsWith(vars[0] + "="))
return true;
return false;
}
/**
* locatePluginService
* locate the service that matches the passed-in information about a service and return the
* base URL the client can use to connect to the target plugin service. If such service
* doesn't exist, this implementation will also start the service.
*
* @param pluginId
* @param command name of the starting script
* @param nginxRules rules to help setup nginx proxying
* @param startedIndicator string indicating that the plugin has started
* @param startedIndicatorStream stream to search for indicator, null defaults to stdout
* @param recordOutput boolean, record out/err streams to output log service or not, null defaults
* to false
* @param waitfor if record output log service is used, string to wait for before logging starts
* @return the base url of the service
* @throws InterruptedException
* @throws IOException
*/
@GET
@Path("/{plugin-id}")
@Produces(MediaType.TEXT_PLAIN)
public Response locatePluginService(
@PathParam("plugin-id") String pluginId,
@QueryParam("command") String command,
@QueryParam("nginxRules") @DefaultValue("rest") String nginxRules,
@QueryParam("startedIndicator") String startedIndicator,
@QueryParam("startedIndicatorStream") @DefaultValue("stdout") String startedIndicatorStream,
@QueryParam("recordOutput") @DefaultValue("false") boolean recordOutput,
@QueryParam("waitfor") String waitfor)
throws InterruptedException, IOException {
PluginConfig pConfig = this.plugins.get(pluginId);
if (pConfig != null && pConfig.isStarted()) {
System.out.println("plugin service " + pluginId +
" already started at" + pConfig.getBaseUrl());
return buildResponse(pConfig.getBaseUrl(), false);
}
String password = RandomStringUtils.random(40, true, true);
Process proc = null;
String restartId = "";
/*
* Only one plugin can be started at a given time since we need to find a free port.
* We serialize starting of plugins and we parallelize nginx configuration reload with the actual plugin
* evaluator start.
*/
synchronized (this) {
// find a port to use for proxypass between nginx and the plugin
final int port = getNextAvailablePort(this.portSearchStart);
final String baseUrl = "/" + urlHash + "/" + generatePrefixedRandomString(pluginId, 12).replaceAll("[\\s]", "");
pConfig = new PluginConfig(port, nginxRules, baseUrl, password);
this.portSearchStart = pConfig.port + 1;
this.plugins.put(pluginId, pConfig);
if (nginxRules.startsWith("ipython")) {
generateIPythonConfig(pluginId, port, password, command);
}
// reload nginx config
restartId = generateNginxConfig();
Process restartproc = Runtime.getRuntime().exec(this.nginxRestartCommand, this.nginxEnv);
startGobblers(restartproc, "restart-nginx-" + pluginId, null, null);
restartproc.waitFor();
String fullCommand = command;
String baseCommand;
String args;
int space = command.indexOf(' ');
if (space > 0) {
baseCommand = command.substring(0, space);
args = command.substring(space); // include space
} else {
baseCommand = command;
args = " ";
}
if (Files.notExists(Paths.get(baseCommand))) {
if (this.pluginLocations.containsKey(pluginId)) {
fullCommand = this.pluginLocations.get(pluginId) + "/" + baseCommand;
}
if (Files.notExists(Paths.get(fullCommand))) {
fullCommand = this.pluginDir + "/" + baseCommand;
if (Files.notExists(Paths.get(fullCommand))) {
throw new PluginServiceNotFoundException("plugin service: " + pluginId + " not found" + " and fail to start it with: " + command);
}
}
}
if (windows()) {
fullCommand = "\"" + fullCommand + "\"";
} else {
fullCommand += args; // XXX should be in windows too?
}
List<String> extraArgs = this.pluginArgs.get(pluginId);
if (extraArgs != null) {
fullCommand += " " + StringUtils.join(extraArgs, " ");
}
fullCommand += " " + Integer.toString(pConfig.port);
String[] env = this.pluginEnvps.get(pluginId);
List<String> envList = new ArrayList<>();
if (env != null) {
for (int i = 0; i < env.length; i++) {
if (!internalEnvar(env[i]))
envList.add(env[i]);
}
} else {
for (Map.Entry<String, String> entry: System.getenv().entrySet()) {
if (!internalEnvar(entry.getKey() + "="))
envList.add(entry.getKey() + "=" + entry.getValue());
}
}
envList.add("beaker_plugin_password=" + password);
envList.add("beaker_core_password=" + this.corePassword);
envList.add("beaker_core_port=" + corePort);
envList.add("beaker_tmp_dir=" + this.nginxServDir);
env = new String[envList.size()];
envList.toArray(env);
if (windows()) {
fullCommand = "python " + fullCommand;
}
System.out.println("Running: " + fullCommand);
proc = Runtime.getRuntime().exec(fullCommand, env);
}
if (startedIndicator != null && !startedIndicator.isEmpty()) {
InputStream is = startedIndicatorStream.equals("stderr") ? proc.getErrorStream() : proc.getInputStream();
InputStreamReader ir = new InputStreamReader(is);
BufferedReader br = new BufferedReader(ir);
String line = "";
while ((line = br.readLine()) != null) {
System.out.println("looking on " + startedIndicatorStream + " found:" + line);
if (line.indexOf(startedIndicator) >= 0) {
System.out.println("Acknowledge " + pluginId + " plugin started");
break;
}
}
if (null == line) {
throw new PluginServiceNotFoundException("plugin service: "
+ pluginId + " failed to start");
}
}
startGobblers(proc, pluginId, recordOutput ? this.outputLogService : null, waitfor);
// check that nginx did actually restart
String url = "http://127.0.0.1:" + this.restartPort + "/restart." + restartId + "/present.html";
try {
spinCheck(url);
if (windows()) Thread.sleep(1000); // XXX unknown race condition
} catch (Throwable t) {
System.err.println("Nginx restart time out plugin =" + pluginId);
this.plugins.remove(pluginId);
if (windows()) {
new WinProcess(proc).killRecursively();
} else {
proc.destroy(); // send SIGTERM
}
throw new NginxRestartFailedException("nginx restart failed.\n" + "url=" + url + "\n" + "message=" + t.getMessage());
}
pConfig.setProcess(proc);
System.out.println("Done starting " + pluginId);
return buildResponse(pConfig.getBaseUrl(), true);
}
@GET
@Path("getAvailablePort")
public int getAvailablePort() {
int port;
synchronized (this) {
port = getNextAvailablePort(this.portSearchStart++);
}
return port;
}
private static Response buildResponse(String baseUrl, boolean created) {
baseUrl = "../.." + baseUrl;
return Response
.status(created ? Response.Status.CREATED : Response.Status.OK)
.entity(baseUrl)
.location(URI.create(baseUrl))
.build();
}
private static boolean spinCheck(String url)
throws IOException, InterruptedException
{
int interval = RESTART_ENSURE_RETRY_INTERVAL;
int totalTime = 0;
while (totalTime < RESTART_ENSURE_RETRY_MAX_WAIT) {
if (Request.Get(url)
.execute()
.returnResponse()
.getStatusLine()
.getStatusCode() == HttpStatus.SC_OK) {
return true;
}
Thread.sleep(interval);
totalTime += interval;
interval *= 1.5;
if (interval > RESTART_ENSURE_RETRY_MAX_INTERVAL)
interval = RESTART_ENSURE_RETRY_MAX_INTERVAL;
}
throw new RuntimeException("Spin check timed out");
}
private static class PluginServiceNotFoundException extends WebApplicationException {
public PluginServiceNotFoundException(String message) {
super(Response.status(Responses.NOT_FOUND)
.entity(message).type("text/plain").build());
}
}
private static class NginxRestartFailedException extends WebApplicationException {
public NginxRestartFailedException(String message) {
super(Response.status(HttpStatus.SC_INTERNAL_SERVER_ERROR)
.entity(message).type("text/plain").build());
}
}
private void addPluginArgs(String plugin, String arg) {
List<String> args = this.pluginArgs.get(plugin);
if (args == null) {
args = new ArrayList<>();
this.pluginArgs.put(plugin, args);
}
args.add(arg);
}
private void writePrivateFile(java.nio.file.Path path, String contents)
throws IOException, InterruptedException
{
if (windows()) {
String p = path.toString();
Thread.sleep(1000); // XXX unknown race condition
try (PrintWriter out = new PrintWriter(p)) {
out.print(contents);
}
return;
}
if (Files.exists(path)) {
Files.delete(path);
}
try (PrintWriter out = new PrintWriter(path.toFile())) {
out.print("");
}
Set<PosixFilePermission> perms = EnumSet.of(PosixFilePermission.OWNER_READ,
PosixFilePermission.OWNER_WRITE);
Files.setPosixFilePermissions(path, perms);
// XXX why is this in a try block?
try (PrintWriter out = new PrintWriter(path.toFile())) {
out.print(contents);
}
}
private String hashIPythonPassword(String password, String pluginId, String command)
throws IOException
{
String cmdBase = pythonBaseCommand(pluginId, command);
Process proc = Runtime.getRuntime().exec(cmdBase + " --hash");
BufferedReader br = new BufferedReader(new InputStreamReader(proc.getInputStream()));
BufferedWriter bw = new BufferedWriter(new OutputStreamWriter(proc.getOutputStream()));
new StreamGobbler(proc.getErrorStream(), "stderr", "ipython-hash", null, null).start();
bw.write("from IPython.lib import passwd\n");
// I have confirmed that this does not go into ipython history by experiment
// but it would be nice if there were a way to make this explicit. XXX
bw.write("print(passwd('" + password + "'))\n");
bw.close();
String hash = br.readLine();
if (null == hash) {
throw new RuntimeException("unable to get IPython hash");
}
return hash;
}
private void generateIPythonConfig(String pluginId, int port, String password, String command)
throws IOException, InterruptedException
{
// Can probably determine exactly what is needed and then just
// make the files ourselves but this is a safe way to get started.
String cmd = pythonBaseCommand(pluginId, command) + " --profile " +
this.nginxServDir + " " + pluginId;
Runtime.getRuntime().exec(cmd).waitFor();
String hash = hashIPythonPassword(password, pluginId, command);
String config = this.ipythonTemplate;
config = config.replace("%(port)s", Integer.toString(port));
config = config.replace("%(hash)s", hash);
java.nio.file.Path targetFile = Paths.get(this.nginxServDir + "/profile_beaker_backend_" + pluginId,
"ipython_notebook_config.py");
writePrivateFile(targetFile, config);
}
private String generateNginxConfig() throws IOException, InterruptedException {
java.nio.file.Path confDir = Paths.get(this.nginxServDir, "conf");
java.nio.file.Path logDir = Paths.get(this.nginxServDir, "logs");
java.nio.file.Path nginxClientTempDir = Paths.get(this.nginxServDir, "client_temp");
if (Files.notExists(confDir)) {
confDir.toFile().mkdirs();
Files.copy(Paths.get(this.nginxDir + "/mime.types"),
Paths.get(confDir.toString() + "/mime.types"));
}
if (Files.notExists(logDir)) {
logDir.toFile().mkdirs();
}
if (Files.notExists(nginxClientTempDir)) {
nginxClientTempDir.toFile().mkdirs();
}
String restartId = RandomStringUtils.random(12, false, true);
String nginxConfig = this.nginxTemplate;
StringBuilder pluginSection = new StringBuilder();
for (PluginConfig pConfig : this.plugins.values()) {
String auth = encoder.encodeBase64String(("beaker:" + pConfig.getPassword()).getBytes());
String nginxRule = pConfig.getNginxRules();
if (this.nginxPluginRules.containsKey(nginxRule)) {
nginxRule = this.nginxPluginRules.get(nginxRule);
} else {
if (nginxRule.equals("rest"))
nginxRule = REST_RULES;
else if (nginxRule.equals("ipython1"))
nginxRule = IPYTHON1_RULES;
else if (nginxRule.equals("ipython2"))
nginxRule = IPYTHON2_RULES;
else {
throw new RuntimeException("unrecognized nginx rule: " + nginxRule);
}
}
nginxRule = nginxRule.replace("%(port)s", Integer.toString(pConfig.getPort()))
.replace("%(auth)s", auth)
.replace("%(base_url)s", pConfig.getBaseUrl());
pluginSection.append(nginxRule + "\n\n");
}
String auth = encoder.encodeBase64String(("beaker:" + this.corePassword).getBytes());
String listenSection;
String authCookieRule;
String startPage;
if (this.publicServer) {
listenSection = "listen " + this.portBase + " ssl;\n";
// XXX should allow name to be set by user in bkConfig
listenSection += "server_name " + InetAddress.getLocalHost().getHostName() + ";\n";
listenSection += "ssl_certificate " + this.nginxServDir + "/ssl_cert.pem;\n";
listenSection += "ssl_certificate_key " + this.nginxServDir + "/ssl_cert.pem;\n";
authCookieRule = "if ($http_cookie !~ \"BeakerAuth=" + this.authCookie + "\") {return 403;}";
startPage = "/login/login.html";
} else {
listenSection = "listen 127.0.0.1:" + this.servPort + ";\n";
authCookieRule = "";
startPage = "/beaker/";
}
nginxConfig = nginxConfig.replace("%(plugin_section)s", pluginSection.toString());
nginxConfig = nginxConfig.replace("%(extra_rules)s", this.nginxExtraRules);
nginxConfig = nginxConfig.replace("%(host)s", InetAddress.getLocalHost().getHostName());
nginxConfig = nginxConfig.replace("%(port_main)s", Integer.toString(this.portBase));
nginxConfig = nginxConfig.replace("%(port_beaker)s", Integer.toString(this.corePort));
nginxConfig = nginxConfig.replace("%(port_clear)s", Integer.toString(this.servPort));
nginxConfig = nginxConfig.replace("%(listen_on)s", this.publicServer ? "*" : "127.0.0.1");
nginxConfig = nginxConfig.replace("%(listen_section)s", listenSection);
nginxConfig = nginxConfig.replace("%(auth_cookie_rule)s", authCookieRule);
nginxConfig = nginxConfig.replace("%(start_page)s", startPage);
nginxConfig = nginxConfig.replace("%(port_restart)s", Integer.toString(this.restartPort));
nginxConfig = nginxConfig.replace("%(auth)s", auth);
nginxConfig = nginxConfig.replace("%(restart_id)s", restartId);
nginxConfig = nginxConfig.replace("%(urlhash)s", urlHash);
nginxConfig = nginxConfig.replace("%(static_dir)s", this.nginxStaticDir.replaceAll("\\\\", "/"));
nginxConfig = nginxConfig.replace("%(nginx_dir)s", this.nginxServDir.replaceAll("\\\\", "/"));
java.nio.file.Path targetFile = Paths.get(this.nginxServDir, "conf/nginx.conf");
writePrivateFile(targetFile, nginxConfig);
return restartId;
}
private static int getNextAvailablePort(int start) {
final int SEARCH_LIMIT = 100;
for (int p = start; p < start + SEARCH_LIMIT; ++p) {
if (isPortAvailable(p)) {
return p;
}
}
throw new RuntimeException("out of ports error");
}
private static String generatePrefixedRandomString(String prefix, int randomPartLength) {
// Use lower case due to nginx bug handling mixed case locations
// (fixed in 1.5.6 but why depend on it).
return prefix.toLowerCase() + "." + RandomStringUtils.random(randomPartLength, false, true);
}
@GET
@Path("getIPythonVersion")
@Produces(MediaType.APPLICATION_JSON)
public String getIPythonVersion(@QueryParam("pluginId") String pluginId,
@QueryParam("command") String command)
throws IOException
{
Process proc;
if (windows()) {
// XXX use ipythonPlugin --version, like generateIPythonConfig does
String cmd = "python " + "\"" + this.pluginDir + "/ipythonPlugins/ipython/ipythonVersion\"";
proc = Runtime.getRuntime().exec(cmd);
} else {
proc = Runtime.getRuntime().exec(pythonBaseCommand(pluginId, command) + " --version");
}
BufferedReader br = new BufferedReader(new InputStreamReader(proc.getInputStream()));
new StreamGobbler(proc.getErrorStream(), "stderr", "ipython-version", null, null).start();
String line = br.readLine();
return line;
}
@GET
@Path("getIPythonPassword")
@Produces(MediaType.APPLICATION_JSON)
public String getIPythonPassword(@QueryParam("pluginId") String pluginId)
{
PluginConfig pConfig = this.plugins.get(pluginId);
if (null == pConfig) {
return "";
}
/* It's OK to return the password because the connection should be
HTTPS and the request is authenticated so only our legit user
should be on the other side. */
return pConfig.password;
}
private static class PluginConfig {
private final int port;
private final String nginxRules;
private Process proc;
private final String baseUrl;
private final String password;
PluginConfig(int port, String nginxRules, String baseUrl, String password) {
this.port = port;
this.nginxRules = nginxRules;
this.baseUrl = baseUrl;
this.password = password;
}
int getPort() {
return this.port;
}
String getBaseUrl() {
return this.baseUrl;
}
String getNginxRules() {
return this.nginxRules;
}
String getPassword() {
return this.password;
}
void setProcess(Process proc) {
this.proc = proc;
}
boolean isStarted () {
return this.proc != null;
}
void shutDown() {
if (this.isStarted()) {
if (windowsStatic()) {
new WinProcess(this.proc).killRecursively();
} else {
this.proc.destroy(); // send SIGTERM
}
}
}
}
private static boolean isPortAvailable(int port) {
ServerSocket ss = null;
try {
InetAddress address = InetAddress.getByName("127.0.0.1");
ss = new ServerSocket(port, 1, address);
// ss = new ServerSocket(port);
ss.setReuseAddress(true);
return true;
} catch (IOException e) {
} finally {
if (ss != null) {
try {
ss.close();
} catch (IOException e) {
/* should not be thrown */
}
}
}
return false;
}
private static void startGobblers(
Process proc,
String name,
OutputLogService outputLogService,
String waitfor) {
StreamGobbler errorGobbler =
new StreamGobbler(proc.getErrorStream(), "stderr", name,
outputLogService, waitfor);
errorGobbler.start();
StreamGobbler outputGobbler =
new StreamGobbler(proc.getInputStream(), "stdout", name,
outputLogService);
outputGobbler.start();
}
}