package org.basex; import static org.basex.core.Text.*; import java.io.File; import java.io.IOException; import java.net.InetAddress; import java.net.InetSocketAddress; import java.net.ServerSocket; import java.net.Socket; import org.basex.core.BaseXException; import org.basex.core.Context; import org.basex.core.Main; import org.basex.core.MainProp; import org.basex.core.Prop; import org.basex.io.in.BufferInput; import org.basex.server.ClientListener; import org.basex.server.ClientSession; import org.basex.server.LocalSession; import org.basex.server.Log; import org.basex.server.LoginException; import org.basex.server.Session; import org.basex.util.Args; import org.basex.util.Performance; import org.basex.util.Token; import org.basex.util.Util; import org.basex.util.hash.TokenIntMap; import org.basex.util.list.StringList; /** * This is the starter class for running the database server. It handles * concurrent requests from multiple users. * * @author BaseX Team 2005-12, BSD License * @author Christian Gruen * @author Andreas Weiler */ public final class BaseXServer extends Main implements Runnable { /** Flag for server activity. */ public volatile boolean running; /** Event server socket. */ ServerSocket esocket; /** Stop file. */ File stop; /** Log. */ Log log; /** EventsListener. */ private EventListener events; /** Temporarily blocked clients. */ private final TokenIntMap blocked = new TokenIntMap(); /** Quiet mode (no logging). */ private boolean quiet; /** Start as daemon. */ private boolean service; /** Stopped flag. */ private volatile boolean stopped; /** Server socket. */ private ServerSocket socket; /** Initial commands. */ private StringList commands; /** * 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 * @param args command-line arguments * @throws IOException I/O exception */ public BaseXServer(final Context ctx, final String... args) throws IOException { super(args, ctx); final MainProp mprop = context.mprop; final int port = mprop.num(MainProp.SERVERPORT); final int eport = mprop.num(MainProp.EVENTPORT); // check if ports are distinct if(port == eport) throw new BaseXException(PORT_TWICE_X, port); final String host = mprop.get(MainProp.SERVERHOST); final InetAddress addr = host.isEmpty() ? null : InetAddress.getByName(host); if(service) { start(port, args); Util.outln(SRV_STARTED); Performance.sleep(1000); return; } if(stopped) { stop(port, eport); Util.outln(SRV_STOPPED); Performance.sleep(1000); return; } try { // execute command-line arguments for(final String c : commands) execute(c); log = new Log(context, quiet); log.write(SRV_STARTED); socket = new ServerSocket(); socket.setReuseAddress(true); socket.bind(new InetSocketAddress(addr, port)); esocket = new ServerSocket(); esocket.setReuseAddress(true); esocket.bind(new InetSocketAddress(addr, eport)); stop = stopFile(port); // show info when server is aborted Runtime.getRuntime().addShutdownHook(new Thread() { @Override public void run() { log.write(SRV_STOPPED); log.close(); Util.outln(SRV_STOPPED); } }); new Thread(this).start(); while(!running) Performance.sleep(100); Util.outln(CONSOLE + (console ? TRY_MORE_X : SRV_STARTED), SERVERMODE); if(console) { console(); quit(); } } catch(final IOException ex) { if(log != null) log.write(ex.getMessage()); throw ex; } } @Override public void run() { running = true; while(running) { try { final Socket s = socket.accept(); if(stop.exists()) { if(!stop.delete()) log.write(Util.info(FILE_NOT_DELETED_X, stop)); quit(); } else { // drop inactive connections final long ka = context.mprop.num(MainProp.KEEPALIVE) * 1000L; if(ka > 0) { final long ms = System.currentTimeMillis(); for(final ClientListener cs : context.sessions) { if(ms - cs.last > ka) cs.quit(); } } new ClientListener(s, context, log, this).start(); } } catch(final IOException ex) { // socket was closed.. break; } } } /** * Generates a stop file for the specified port. * @param port server port * @return stop file */ private static File stopFile(final int port) { return new File(Prop.TMP, Util.name(BaseXServer.class) + port); } @Override protected void quit() throws IOException { if(!running) return; running = false; for(final ClientListener cs : context.sessions) cs.quit(); super.quit(); context.close(); try { // close interactive input if server was stopped by another process if(console) System.in.close(); esocket.close(); socket.close(); } catch(final IOException ex) { log.write(ex.getMessage()); Util.stack(ex); } console = false; } @Override protected Session session() { if(session == null) session = new LocalSession(context, out); return session; } @Override protected void parseArguments(final String[] args) throws IOException { final Args arg = new Args(args, this, SERVERINFO, Util.info(CONSOLE, SERVERMODE)); commands = new StringList(); boolean daemon = false; while(arg.more()) { if(arg.dash()) { switch(arg.next()) { case 'c': // send database commands commands.add(arg.string()); break; case 'd': // activate debug mode context.mprop.set(MainProp.DEBUG, true); break; case 'D': // hidden flag: daemon mode daemon = true; break; case 'e': // parse event port context.mprop.set(MainProp.EVENTPORT, arg.number()); break; case 'i': // activate interactive mode console = true; break; case 'p': // parse server port context.mprop.set(MainProp.SERVERPORT, arg.number()); break; case 'S': // set service flag service = !daemon; break; case 'z': // suppress logging quiet = true; break; default: arg.usage(); } } else { if(arg.string().equalsIgnoreCase("stop")) { stopped = true; } else { arg.usage(); } } } } /** * Stops the server of this instance. * @throws IOException I/O exception */ public void stop() throws IOException { final int port = context.mprop.num(MainProp.SERVERPORT); final int eport = context.mprop.num(MainProp.EVENTPORT); stop(port, eport); } // 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 { // check if server is already running (needs some time) if(ping(LOCALHOST, port)) throw new BaseXException(SRV_RUNNING); Util.start(BaseXServer.class, args); // try to connect to the new server instance for(int c = 0; c < 10; ++c) { if(ping(LOCALHOST, port)) return; Performance.sleep(100); } throw new BaseXException(CONNECTION_ERROR); } /** * 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 new ClientSession(host, port, "", ""); return false; } catch(final IOException ex) { // if login was checked, server is running return ex instanceof LoginException; } } /** * Stops the server. * @param port server port * @param eport event port * @throws IOException I/O exception */ public static void stop(final int port, final int eport) throws IOException { final File stop = stopFile(port); try { stop.createNewFile(); new Socket(LOCALHOST, eport).close(); new Socket(LOCALHOST, port).close(); // check if server was really stopped while(ping(LOCALHOST, port)) Performance.sleep(50); Performance.sleep(50); } catch(final IOException ex) { stop.delete(); throw ex; } } /** * Registers the client and calculates the delay after unsuccessful logins. * @param client client address * @return delay */ public int block(final byte[] client) { synchronized(blocked) { int delay = blocked.value(client); delay = delay == -1 ? 1 : Math.min(delay, 1024) * 2; blocked.add(client, delay); return delay; } } /** * Resets the login delay after successful login. * @param client client address */ public void unblock(final byte[] client) { synchronized(blocked) { blocked.delete(client); } } /** * Initializes the event listener. */ public void initEvents() { if(events == null) { events = new EventListener(); events.start(); } } /** * Inner class to listen for event registrations. * * @author BaseX Team 2005-12, BSD License * @author Andreas Weiler */ final class EventListener extends Thread { @Override public void run() { while(running) { try { final Socket es = esocket.accept(); if(stop.exists()) { esocket.close(); break; } final BufferInput bi = new BufferInput(es.getInputStream()); final long id = Token.toLong(bi.readString()); for(final ClientListener s : context.sessions) { if(s.getId() == id) { s.register(es); break; } } } catch(final IOException ex) { break; } } } } }