package edu.stanford.nlp.sempre.interactive; import java.io.IOException; import java.io.OutputStreamWriter; import java.io.PrintWriter; import java.io.UnsupportedEncodingException; import java.net.HttpCookie; import java.net.InetSocketAddress; import java.net.URI; import java.net.URLDecoder; import java.net.URLEncoder; import java.time.LocalDateTime; import java.util.ArrayList; import java.util.HashMap; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.Future; import java.util.concurrent.LinkedBlockingQueue; import java.util.concurrent.ThreadPoolExecutor; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicLong; import com.google.common.base.Strings; import com.sun.net.httpserver.Headers; import com.sun.net.httpserver.HttpExchange; import com.sun.net.httpserver.HttpHandler; import com.sun.net.httpserver.HttpServer; import edu.stanford.nlp.sempre.Derivation; import edu.stanford.nlp.sempre.ErrorValue; import edu.stanford.nlp.sempre.Example; import edu.stanford.nlp.sempre.Json; import edu.stanford.nlp.sempre.Master; import edu.stanford.nlp.sempre.Session; import edu.stanford.nlp.sempre.StringValue; import edu.stanford.nlp.sempre.Value; import fig.basic.IOUtils; import fig.basic.LogInfo; import fig.basic.MapUtils; import fig.basic.Option; /** * JsonServer, interactive learning queries run through this. All the logs are * handled here. * * @author sidaw */ public class InteractiveServer { public static class Options { @Option public int port = 8400; @Option public int numThreads = 4; @Option public int verbose = 1; @Option public int maxCandidates = Integer.MAX_VALUE; @Option public String queryLogPath = "./int-output/query.log"; @Option public String responseLogPath = "./int-output/response.log"; @Option public String fullResponseLogPath; @Option public int maxExecutionTime = 10; // in seconds } public static Options opts = new Options(); private static Object queryLogLock = new Object(); private static Object responseLogLock = new Object(); private static AtomicLong queryCounter = new AtomicLong(); Master master; class Handler implements HttpHandler { @Override public void handle(HttpExchange exchange) { try { new ExchangeState(exchange); } catch (Exception e) { e.printStackTrace(); } } } class ExchangeState { // Input HttpExchange exchange; Map<String, String> reqParams = new HashMap<>(); String remoteHost; // For header HttpCookie cookie; boolean isNewSession; // For writing main content public ExchangeState(HttpExchange exchange) throws IOException { this.exchange = exchange; URI uri = exchange.getRequestURI(); this.remoteHost = exchange.getRemoteAddress().getHostName(); // Don't use uri.getQuery: it can't distinguish between '+' and '-' String[] tokens = uri.toString().split("\\?"); if (tokens.length == 2) { for (String s : tokens[1].split("&")) { String[] kv = s.split("=", 2); try { String key = URLDecoder.decode(kv[0], "UTF-8"); String value = URLDecoder.decode(kv[1], "UTF-8"); // logs("%s => %s", key, value); reqParams.put(key, value); } catch (UnsupportedEncodingException e) { throw new RuntimeException(e); } } } // do not decode sessionId, keep it filename and lisptree friendly String sessionId = URLEncoder.encode(MapUtils.get(reqParams, "sessionId", ""), "UTF-8"); if (sessionId != null) { isNewSession = false; } else { isNewSession = true; } if (opts.verbose >= 2) logs("GET %s from %s (%ssessionId=%s)", uri, remoteHost, isNewSession ? "new " : "", sessionId); String uriPath = uri.getPath(); if (uriPath.equals("/")) uriPath += "index.html"; if (uriPath.equals("/sempre")) { handleQuery(sessionId); } else { // getFile(opts.basePath + uriPath); security } exchange.close(); } String getMimeType(String path) { String[] tokens = path.split("\\."); String ext = tokens[tokens.length - 1]; if (ext.equals("html")) return "text/html"; if (ext.equals("css")) return "text/css"; if (ext.equals("jpeg")) return "image/jpeg"; if (ext.equals("gif")) return "image/gif"; return "text/plain"; } void setHeaders(String mimeType) throws IOException { Headers headers = exchange.getResponseHeaders(); headers.set("Content-Type", mimeType); headers.set("Access-Control-Allow-Origin", "*"); if (isNewSession && cookie != null) headers.set("Set-Cookie", cookie.toString()); exchange.sendResponseHeaders(200, 0); } Map<String, Object> makeJson(Master.Response response) { Map<String, Object> json = new HashMap<String, Object>(); json.put("stats", response.stats); if (response.lines != null) { json.put("lines", response.lines); } if (response.getExample() != null) { List<Object> items = new ArrayList<Object>(); json.put("candidates", items); List<Derivation> allCandidates = response.getExample().getPredDerivations(); Derivation.sortByScore(allCandidates); if (allCandidates != null) { if (allCandidates.size() >= InteractiveServer.opts.maxCandidates) { response.lines.add(String.format("Exceeded max options: (current: %d / max: %d) ", allCandidates.size(), InteractiveServer.opts.maxCandidates)); allCandidates = allCandidates.subList(0, InteractiveServer.opts.maxCandidates); } for (Derivation deriv : allCandidates) { Map<String, Object> item = new HashMap<String, Object>(); Value value = deriv.getValue(); if (value instanceof StringValue) item.put("value", ((StringValue) value).value); else if (value instanceof ErrorValue) item.put("value", ((ErrorValue) value).sortString()); else if (value != null) item.put("value", value.sortString()); else item.put("value", "[[]]"); item.put("score", deriv.getScore()); item.put("prob", deriv.getProb()); item.put("anchored", deriv.allAnchored); // used only anchored rules item.put("formula", deriv.formula.toLispTree().toString()); items.add(item); } } } return json; } // This should be concurrent Master.Response processQuery(Session session, String query) { String message = null; Master.Response response = master.new Response(); ExecutorService executor = Executors.newSingleThreadExecutor(); Future<Master.Response> future = executor.submit(() -> master.processQuery(session, query)); long startTime = System.nanoTime(); try { // most exceptions should be handled in InteractiveMaster // so the response can be more specific response = future.get(opts.maxExecutionTime, TimeUnit.SECONDS); } catch (Throwable e) { e.printStackTrace(); message = e.toString(); response.lines.add(String.format("Exceeded the maximum allowed time: %ss", opts.maxExecutionTime)); response.stats.put("uncaught_error", message); LogInfo.flush(); LogInfo.resetInfos(); } finally { future.cancel(true); executor.shutdown(); long endTime = System.nanoTime(); response.stats.put("walltime", (endTime - startTime) / 1.0e9); response.stats.put("count", queryCounter.get()); } return response; } void handleQuery(String sessionId) throws IOException { String query = reqParams.get("q"); long queryNumber = queryCounter.incrementAndGet(); Session session = master.getSession(sessionId); session.reqParams = reqParams; session.remoteHost = remoteHost; session.format = "json"; LocalDateTime queryTime = LocalDateTime.now(); synchronized (queryLogLock) { // write the query log Map<String, Object> jsonMap = new LinkedHashMap<>(); jsonMap.put("count", queryNumber); jsonMap.put("q", query); // jsonMap.put("remote", remoteHost); // jsonMap.put("time", queryTime.toString()); jsonMap.put("sessionId", sessionId); reqParams.remove("q"); jsonMap.putAll(reqParams); if (session.isLogging()) { logLine(opts.queryLogPath, Json.writeValueAsStringHard(jsonMap)); } else { logLine(opts.queryLogPath + ".sandbox", Json.writeValueAsStringHard(jsonMap)); } } // If JSON, don't store cookies. if (query == null) query = "null"; logs("Server.handleQuery %s: %s", session.id, query); // Print header setHeaders("application/json"); Master.Response masterResponse = null; if (query != null) { masterResponse = processQuery(session, query); } Map<String, Object> responseMap = null; { PrintWriter out = new PrintWriter(new OutputStreamWriter(exchange.getResponseBody())); if (masterResponse != null) { // Render answer Example ex = masterResponse.getExample(); responseMap = makeJson(masterResponse); out.println(Json.writeValueAsStringHard(responseMap)); } out.close(); } synchronized (responseLogLock) { // write the response log log Map<String, Object> jsonMap = new LinkedHashMap<>(); LocalDateTime responseTime = LocalDateTime.now(); // jsonMap.put("responseTime", responseTime.toString()); jsonMap.put("time", queryTime.toString()); jsonMap.put("ms", String.format("%.3f", java.time.Duration.between(queryTime, responseTime).toNanos() / 1.0e6)); jsonMap.put("sessionId", sessionId); jsonMap.put("q", query); // backwards compatibility... jsonMap.put("lines", responseMap.get("lines")); if (session.isLogging()) { logLine(opts.responseLogPath, Json.writeValueAsStringHard(jsonMap)); if (!Strings.isNullOrEmpty(opts.fullResponseLogPath)) { jsonMap.put("candidates", responseMap.get("candidates")); logLine(opts.fullResponseLogPath, Json.writeValueAsStringHard(jsonMap)); } } else { logLine(opts.responseLogPath + ".sandbox", Json.writeValueAsStringHard(jsonMap)); if (!Strings.isNullOrEmpty(opts.fullResponseLogPath)) { jsonMap.put("candidates", responseMap.get("candidates")); logLine(opts.fullResponseLogPath + ".sandbox", Json.writeValueAsStringHard(jsonMap)); } // LogInfo.log(Json.writeValueAsStringHard(jsonMap)); } } } void logLine(String path, String line) { PrintWriter out; try { out = IOUtils.openOutAppend(path); out.println(line); out.close(); } catch (IOException e) { // TODO Auto-generated catch block e.printStackTrace(); } } } private void logs(String s, Object... args) { }; public InteractiveServer(Master master) { this.master = master; } public void run() { try { String hostname = fig.basic.SysInfoUtils.getHostName(); HttpServer server = HttpServer.create(new InetSocketAddress(opts.port), 10); // generous timeout here ExecutorService pool = new ThreadPoolExecutor(opts.numThreads, opts.numThreads, 120, TimeUnit.SECONDS, new LinkedBlockingQueue<Runnable>()); // Executors.newFixedThreadPool(opts.numThreads); server.createContext("/", new Handler()); server.setExecutor(pool); server.start(); LogInfo.logs("JSON Server (%d threads) started at http://%s:%s/sempre", opts.numThreads, hostname, opts.port); LogInfo.log("Press Ctrl-D to terminate."); LogInfo.begin_threads(); while (LogInfo.stdin.readLine() != null) { } LogInfo.log("Shutting down server..."); server.stop(0); LogInfo.log("Shutting down executor pool..."); pool.shutdown(); LogInfo.end_threads(); } catch (IOException e) { throw new RuntimeException(e); } } }