package org.basex; import static org.basex.core.Text.*; import java.io.*; import java.net.*; import java.util.*; import org.basex.api.client.*; import org.basex.core.*; import org.basex.io.*; import org.basex.server.*; import org.basex.server.Log.LogType; import org.basex.util.*; import org.basex.util.list.*; /** * This is the starter class for running the database server. It handles * concurrent requests from multiple users. * * @author BaseX Team 2005-17, BSD License * @author Christian Gruen * @author Andreas Weiler */ public final class BaseXServer extends CLI implements Runnable { /** New sessions. */ private final HashSet<ClientListener> authorizing = new HashSet<>(); /** Indicates if server is running. */ private volatile boolean running; /** Indicates if server is to be stopped. */ private volatile boolean stop; /** Initial commands. */ private StringList commands; /** Server socket. */ private ServerSocket socket; /** Start as service. */ private boolean service; /** Daemon flag. */ private boolean daemon; /** Quiet flag. */ private boolean quiet; /** Stop file. */ private IOFile stopFile; /** * Main method, launching the server process. * Command-line arguments are listed with the {@code -h} argument. * @param args command-line arguments */ public static void main(final String... args) { try { new BaseXServer(args); } catch(final IOException ex) { Util.errln(ex); System.exit(1); } } /** * Constructor. * @param args command-line arguments * @throws IOException I/O exception */ public BaseXServer(final String... args) throws IOException { this(null, args); } /** * Constructor. * @param ctx database context (can be {@code null}) * @param args command-line arguments * @throws IOException I/O exception */ public BaseXServer(final Context ctx, final String... args) throws IOException { super(args, ctx); final StaticOptions sopts = context.soptions; final int port = sopts.get(StaticOptions.SERVERPORT); final String host = sopts.get(StaticOptions.SERVERHOST); final InetAddress addr = host.isEmpty() ? null : InetAddress.getByName(host); if(stop) { stop(); if(!quiet) Util.outln(SRV_STOPPED_PORT_X, port); // keep message visible for a while Performance.sleep(1000); return; } if(service) { start(port, args); if(!quiet) Util.outln(SRV_STARTED_PORT_X, port); Performance.sleep(1000); return; } try { // execute initial command-line arguments for(final String cmd : commands) execute(cmd, null); socket = new ServerSocket(); socket.setReuseAddress(true); socket.bind(new InetSocketAddress(addr, port)); stopFile = stopFile(getClass(), port); } catch(final BindException ex) { context.log.writeServer(LogType.ERROR, Util.message(ex)); Util.debug(ex); throw new BaseXException(SRV_RUNNING_X, port); } catch(final IOException ex) { throw ex; } catch(final Exception ex) { context.log.writeServer(LogType.ERROR, Util.message(ex)); Util.debug(ex); throw new BaseXException(ex.getLocalizedMessage()); } new Thread(this).start(); // show info that server has been started final String startX = Util.info(SRV_STARTED_PORT_X, port); if(!quiet) { if(!daemon) Util.outln(header()); Util.outln(startX); } context.log.writeServer(LogType.OK, startX); // show info when server is aborted Runtime.getRuntime().addShutdownHook(new Thread() { @Override public void run() { close(); } }); } @Override public void run() { running = true; while(running) { try { final Socket s = socket.accept(); if(stopFile.exists()) { close(); } else { // drop inactive connections final long ka = context.soptions.get(StaticOptions.KEEPALIVE) * 1000L; if(ka > 0) { final long ms = System.currentTimeMillis(); for(final ClientListener cs : context.sessions) { if(ms - cs.last > ka) cs.close(); } } // create client listener, stop authentication after timeout final ClientListener cl = new ClientListener(s, context, this); if(ka > 0) { cl.timeout.schedule(new TimerTask() { @Override public void run() { cl.close(); } }, ka); authorizing.add(cl); } cl.start(); } } catch(final SocketException ex) { Util.debug(ex); break; } catch(final Throwable ex) { // socket may have been unexpectedly closed Util.errln(ex); context.log.writeServer(LogType.ERROR, Util.message(ex)); break; } } } /** * Shuts down the server. */ private synchronized void close() { if(!running) return; for(final ClientListener cl : authorizing) { remove(cl); cl.close(); } context.sessions.close(); try { // close interactive input if server was stopped by another process socket.close(); } catch(final IOException ex) { Util.errln(ex); context.log.writeServer(LogType.ERROR, Util.message(ex)); } final int port = context.soptions.get(StaticOptions.SERVERPORT); final String stopX = Util.info(SRV_STOPPED_PORT_X, port); if(!quiet) Util.outln(stopX); context.log.writeServer(LogType.OK, stopX); // close database context context.close(); if(!stopFile.delete()) { context.log.writeServer(LogType.ERROR, Util.info(FILE_NOT_DELETED_X, stopFile)); } running = false; } @Override protected void parseArgs() throws IOException { final MainParser arg = new MainParser(this); commands = new StringList(); while(arg.more()) { if(arg.dash()) { switch(arg.next()) { case 'c': // send database commands commands.add(arg.string()); break; case 'd': // activate debug mode Prop.debug = true; break; case 'D': // hidden flag: daemon mode daemon = true; break; case 'n': // parse host the server is bound to context.soptions.set(StaticOptions.SERVERHOST, arg.string()); break; case 'p': // parse server port context.soptions.set(StaticOptions.SERVERPORT, arg.number()); break; case 'q': // quiet flag (hidden) quiet = true; break; case 'S': // set service flag service = !daemon; break; case 'z': // suppress logging context.soptions.set(StaticOptions.LOG, false); break; default: throw arg.usage(); } } else { if(S_STOP.equalsIgnoreCase(arg.string())) { stop = true; } else { throw arg.usage(); } } } } /** * Stops the server. * @throws IOException I/O exception */ public void stop() throws IOException { final StaticOptions sopts = context.soptions; final int port = sopts.get(StaticOptions.SERVERPORT); final String host = sopts.get(StaticOptions.SERVERHOST); stop(host.isEmpty() ? S_LOCALHOST : host, port); } // STATIC METHODS =========================================================== /** * Starts the database server in a separate process. * @param port server port * @param args command-line arguments * @throws BaseXException database exception */ public static void start(final int port, final String... args) throws BaseXException { // start server and check if it caused an error message final String error = Util.error(Util.start(BaseXServer.class, args), 2000); if(error != null) throw new BaseXException(error.trim()); // try to connect to the new server instance if(!ping(S_LOCALHOST, port)) throw new BaseXException(CONNECTION_ERROR_X, port); } /** * Checks if a server is running. * @param host host * @param port server port * @return boolean success */ public static boolean ping(final String host, final int port) { try { // connect server with invalid login data try(ClientSession cs = new ClientSession(host, port, "", "")) { } return false; } catch(final LoginException ex) { // if login was checked, server is running Util.debug(ex); return true; } catch(final IOException ex) { Util.debug(ex); return false; } } /** * Stops the server. * @param host server host * @param port server port * @throws IOException I/O exception */ public static void stop(final String host, final int port) throws IOException { // create stop file final IOFile stopFile = stopFile(BaseXServer.class, port); stopFile.touch(); // try to connect the server try(Socket s = new Socket(host, port)) { } catch(final ConnectException ex) { Util.debug(ex); stopFile.delete(); throw new IOException(Util.info(CONNECTION_ERROR_X, port)); } // wait until server was stopped do Performance.sleep(10); while(stopFile.exists()); } /** * Removes a client listener that is waiting for authentication. * @param client client to be removed */ public void remove(final ClientListener client) { synchronized(authorizing) { client.timeout.cancel(); authorizing.remove(client); } } @Override public String header() { return Util.info(S_CONSOLE_X, S_SERVER); } @Override public String usage() { return S_SERVERINFO; } }