package org.seqcode.data.readdb; import java.net.*; import java.util.*; import java.util.logging.*; import java.io.*; import org.apache.commons.cli.*; /** * <p>ReadDB server class. Provides configuration-type information, but * pushes all the request handling off onto ServerTask. Usage: * <pre>java org.seqcode.data.readdb.Server --datadir /path/to/data</pre> * * <p>Optional parameters * <ul> * <li>--port 52000 port to listen on * <li>--threads 5 number of threads to start to handle client requests * <li>--cachesize 100 number of chromosomes to keep files open for * <li>--maxconn 1000 maximum number of client connections * <li>--sleepiness 2 how sleepy the server should be waiting for input. Lower values use more CPU but improve responsiveness * <li>--idlelimit 24 number of hours after which idle task connections are closed * <li>--help print the usage message and exit * */ public class Server { public static final String SaslMechanisms[] = {"CRAM-MD5","DIGEST-MD5"}; private Logger logger; private int port; private int numThreads, cacheSize, maxConnections, sleepiness, taskIdleLimit; private boolean debug; /* topdir is the top-level directory for our data files. pwfile is "${topdir}/users.txt" and groupfile is "${topdir}/groups.txt" */ private String topdir, pwfile, groupfile; private boolean keepRunning; private Dispatch dispatch; private Map<String,Set<String>> groups; // BUFFERLEN should be a multiple of 8 to avoid problems with partial ints, floats, or doubles // in buffers when the buffer is allocated in bytes. public static final int BUFFERLEN = 8192 * 16; private LRUCache<Header> singleHeaders; private LRUCache<Header> pairedHeaders; private LRUCache<SingleHits> singleHits; private LRUCache<PairedHits> pairedHits; private LRUCache<AlignmentACL> acls; private ServerSocket socket; public Server () { port = 52000; sleepiness = 4; numThreads = 5; cacheSize = numThreads * 20; maxConnections = 1000; taskIdleLimit = 24; topdir = "/tmp"; keepRunning = true; logger = Logger.getLogger("org.seqcode.data.readdb.Server"); logger.log(Level.INFO,"created Server"); } public void parseArgs(String[] args) throws ParseException { Options options = new Options(); options.addOption("p","port",true,"port to listen on"); options.addOption("t","threads",true,"number of threads to spawn"); options.addOption("d","datadir",true,"directory to use for data"); options.addOption("D","debug",false,"provide debugging output"); options.addOption("C","cachesize",true,"how many files to keep open (this value times up to 18)"); options.addOption("M","maxconn",true,"how many connections are allowed"); options.addOption("S","sleepiness",true,"how sleepy the server should be while waiting for input. 1-100"); options.addOption("L","idlelimit",true,"number of hours after which to close idle connections. Negative sets no limit."); options.addOption("h","help",false,"print help message"); CommandLineParser parser = new GnuParser(); CommandLine line = parser.parse( options, args, false ); if (line.hasOption("help")) { printHelp(); System.exit(1); } if (line.hasOption("port")) { port = Integer.parseInt(line.getOptionValue("port")); } if (line.hasOption("threads")) { numThreads = Integer.parseInt(line.getOptionValue("threads")); cacheSize = 10 * numThreads; } if (line.hasOption("datadir")) { topdir = line.getOptionValue("datadir"); } if (line.hasOption("cachesize")) { cacheSize = Integer.parseInt(line.getOptionValue("cachesize")); } if (line.hasOption("maxconn")) { maxConnections = Integer.parseInt(line.getOptionValue("maxconn")); } if (line.hasOption("sleepiness")) { sleepiness = Integer.parseInt(line.getOptionValue("sleepiness")); if (sleepiness < 1) { sleepiness = 1; } if (sleepiness > 100) { sleepiness = 100; } } if (line.hasOption("idlelimit")) { taskIdleLimit = Integer.parseInt(line.getOptionValue("idlelimit")); } singleHits = new LRUCache<SingleHits>(cacheSize); pairedHits = new LRUCache<PairedHits>(cacheSize); singleHeaders = new LRUCache<Header>(cacheSize); pairedHeaders = new LRUCache<Header>(cacheSize); acls = new LRUCache<AlignmentACL>(cacheSize); debug = line.hasOption("debug"); logger.log(Level.INFO,String.format("Server parsed args: port %d, threads %d, directory %s",port,numThreads,topdir)); pwfile = topdir + System.getProperty("file.separator") + "users.txt"; groupfile = topdir + System.getProperty("file.separator") + "groups.txt"; } public void printHelp() { System.out.println("ReadDB server process"); System.out.println("usage: java org.seqcode.data.readdb.Server --datadir /path/to/datadir --port 52000"); System.out.println(" [--threads 5] use this number of worker threads to process requests."); System.out.println(" [--cachesize 100] number of datasets to keep open. Actual number of open files will be"); System.out.println(" up to 18 times this value"); System.out.println(" [--maxconn 250] maximum number of open connections"); System.out.println(" [--debug] print debugging output"); System.out.println(" [--sleepiness 4] (1-100) higher values use less CPU when idle but may incur more delay in processing requests"); } public static void main(String args[]) throws Exception { Server server = new Server(); server.parseArgs(args); server.readAndProcessGroupsFile(); server.listen(); System.exit(0); } public boolean keepRunning() { return keepRunning; } public void keepRunning(boolean k) { keepRunning = k; if (keepRunning == false && socket != null) { try { socket.close(); } catch (IOException e) { e.printStackTrace(); } } } public boolean debug() {return debug;} public int getSleepiness() {return sleepiness;} public void listen() throws IOException { Thread t = new Thread(new CacheGCHook(logger)); t.start(); dispatch = new Dispatch(this,numThreads, maxConnections); t = new Thread(dispatch); t.start(); socket = new ServerSocket(port); socket.setReuseAddress(true); socket.setReceiveBufferSize(BUFFERLEN); socket.setSoTimeout(1000*3600*24); while (keepRunning) { try { Socket s = socket.accept(); logger.log(Level.INFO,"accepted from " + s.getInetAddress()); s.setSoLinger(false,0); ServerTask st = new ServerTask(this,s, taskIdleLimit*1000*3600); if (debug) { System.err.println("New Task is " + st); } dispatch.addWork(st); }catch (SocketTimeoutException ste){ System.err.println("Connection timeout: "+ste.getMessage()); }catch (SocketException se){ System.err.println("Connection error: "+se.getMessage()); }catch (IOException e) { e.printStackTrace(); } } } public Logger getLogger() {return logger;} public String getTopDir() { return topdir; } public String cleanStringForFilename(String i) { return i.replaceAll("[^A-Za-z0-9_\\-\\+]","_"); } /* where to find the header file for this alignment */ public String getAlignmentDir(String alignID) { alignID = cleanStringForFilename(alignID); return getTopDir() + System.getProperty("file.separator") + alignID; } public String getACLFileName(String alignID) { return getAlignmentDir(alignID) + System.getProperty("file.separator") + "acl.txt"; } public String getDefaultACLFileName() { return getTopDir() + "defaultACL.txt"; } public String getSingleHeaderFileName(String alignID, int chromID, boolean isType2) { return getAlignmentDir(alignID) + System.getProperty("file.separator") + chromID + (isType2 ? ".singlet2index" : ".singleindex"); } public String getPairedHeaderFileName(String alignID, int chromID, boolean isLeft) { return getAlignmentDir(alignID) + System.getProperty("file.separator") + chromID + ".paired" + (isLeft ? "left" : "right") + "index"; } public Set<Integer> getChroms(String alignID, boolean isType2, boolean isPaired, boolean isLeft) { File directory = new File(getAlignmentDir(alignID)); File[] files = directory.listFiles(); if (files == null) { return null; } Set<Integer> output = new HashSet<Integer>(); String suffix = isPaired ? (isLeft ? ".pairedleftindex" : ".pairedrightindex") : (isType2 ? ".singlet2index" : ".singleindex"); for (int i = 0; i < files.length; i++) { int index = files[i].getName().indexOf(suffix); if (index > 0) { output.add(Integer.parseInt(files[i].getName().substring(0,index))); } } return output; } /** * Returns the requested Hits object. Creates it or retrieves from cache. * Client code is responsible for locking the file as necessary. */ public SingleHits getSingleHits(String alignID, int chrom, boolean isType2) throws IOException, SecurityException, FileNotFoundException { String key = alignID + chrom + isType2; SingleHits output = singleHits.get(key); if (output == null) { String prefix = getAlignmentDir(alignID) + System.getProperty("file.separator"); output = new SingleHits(prefix,chrom,isType2); singleHits.add(key, output); } return output; } public PairedHits getPairedHits(String alignID, int chrom, boolean isLeft) throws IOException, SecurityException, FileNotFoundException { String key = alignID + chrom + isLeft; PairedHits output = pairedHits.get(key); if (output == null) { String prefix = getAlignmentDir(alignID) + System.getProperty("file.separator"); output = new PairedHits(prefix, chrom, isLeft); pairedHits.add(key, output); } return output; } /** * Returns the requested Header object. Creates it or retrieves from cache. * Client code is responsible for locking the file as necessary. */ public Header getSingleHeader(String alignID, int chromID, boolean isType2) throws IOException { String key = alignID + chromID +isType2; Header output = singleHeaders.get(key); if (output == null) { output = Header.readIndexFile(getSingleHeaderFileName(alignID,chromID, isType2)); singleHeaders.add(key, output); } return output; } public Header getPairedHeader(String alignID, int chromID, boolean isLeft) throws IOException { String key = alignID + chromID + isLeft; Header output = pairedHeaders.get(key); if (output == null) { output = Header.readIndexFile(getPairedHeaderFileName(alignID,chromID,isLeft)); pairedHeaders.add(key, output); } return output; } /** * Returns the requested ACL object. Creates it or retrieves from cache. * Client code is responsible for locking the file as necessary. */ public AlignmentACL getACL(String alignID) throws IOException { AlignmentACL output = acls.get(alignID); if (output == null) { output = new AlignmentACL(getACLFileName(alignID)); acls.add(alignID,output); } return output; } public void removeSingleHits(String alignID, int chromID, boolean isType2) { singleHits.remove(alignID + chromID+ isType2); } public void removePairedHits(String alignID, int chromID, boolean isLeft) { pairedHits.remove(alignID + chromID + isLeft); } public void removeSingleHeader(String alignID, int chromID, boolean isType2) { singleHeaders.remove(alignID + chromID + isType2); } public void removePairedHeader(String alignID, int chromID, boolean isLeft) { pairedHeaders.remove(alignID + chromID + isLeft); } public void removeACL(String alignID) {acls.remove(alignID);} protected void printCacheContents() { singleHeaders.printKeys(); pairedHeaders.printKeys(); singleHits.printKeys(); pairedHits.printKeys(); acls.printKeys(); } /** * Returns true iff this princ is allowed * to create alignments. * * Currently implemented as members of the * cancreate group */ public boolean canCreate(String username) { return groupContains(username, "cancreate"); } /** * Returns true iff this princ is a server admin. * Server admins can shut the server down and have read/write/admin access to any data. * * Currently implemented as members of the admin * group. */ public boolean isAdmin(String username) { return groupContains(username, "admin"); } /* Authenticate stuff for reading group and password files */ public boolean groupContains(String username, String group) { if (!groups.containsKey(group)) { return false; } return groups.get(group).contains(username); } /** * The groups file has lines of the form * groupname:user1 user2 user3 @othergroup user4 * * Group names that are also usernames are removed * since ACLs can contain either users or groups and we don't * want confusion. * */ public Map<String,Set<String>> readGroupsFile() throws IOException { Map<String,Set<String>> output = new HashMap<String,Set<String>>(); File f = new File(groupfile); if (!f.exists()) { logger.log(Level.WARNING,"No Groups file found: " + groupfile); throw new IOException("No Groups file found"); } BufferedReader reader = new BufferedReader(new FileReader(groupfile)); String line = null; while ((line = reader.readLine()) != null ) { String pieces[] = line.split("\\s*\\:\\s*"); String groupname = pieces[0]; if (pieces.length == 1) { continue;} // empty group pieces = pieces[1].split("\\s+"); Set<String> members = output.containsKey(groupname) ? output.get(groupname) : new HashSet<String>(); for (int i = 0; i < pieces.length; i++) { if (!members.contains(pieces[i])) { members.add(pieces[i]); } } output.put(groupname,members); } reader.close(); return output; } public void readAndProcessGroupsFile() throws IOException { groups = readGroupsFile(); expand(groups, new HashMap<String,Set<String>>()); for (String u : getUserNames()) { groups.remove(u); } } public void addToGroup(ServerTask t, String group, String princ) throws IOException { System.err.println("Adding " + princ + " to " + group); Lock.readLock("group.txt"); Map<String,Set<String>> rawgroups = readGroupsFile(); if (!rawgroups.containsKey(group)) { Set<String> s = new HashSet<String>(); s.add(princ); rawgroups.put(group, s); } if (!rawgroups.get(group).contains(princ)) { rawgroups.get(group).add(princ); } System.err.println("got write lock"); File gfile = File.createTempFile("tmp",".groups"); PrintWriter pw = new PrintWriter(gfile); for (String g : rawgroups.keySet()) { pw.print(g + ":"); for (String p : rawgroups.get(g)) { pw.print(" " + p); } pw.println(); } System.err.println("Done. rereading"); pw.close(); Lock.writeLock("group.txt"); gfile.renameTo(new File(groupfile)); readAndProcessGroupsFile(); } /** * processes recursive group memberships */ private void expand(Map<String, Set<String>> toexpand, Map<String, Set<String>> expanded) { boolean keepgoing = false; for (String g : toexpand.keySet()) { if (!expanded.containsKey(g)) { expanded.put(g,new HashSet<String>()); } for (String m : toexpand.get(g)) { if (m.matches("^@")) { toexpand.get(g).remove(m); String othergroup = m.replaceAll("^@",""); if (!expanded.get(g).contains(m)) { expanded.get(g).add(m); if (!toexpand.get(g).containsAll(toexpand.get(othergroup))) { toexpand.get(g).addAll(toexpand.get(othergroup)); } } } } for (String m : toexpand.get(g)) { if (m.matches("^@") && !expanded.get(g).contains(m)) { keepgoing = true; } } } if (keepgoing) { logger.log(Level.INFO,"Recursing in expand"); expand(toexpand,expanded); } } /** returns the password for the specified user, or * null if the user is unknown * * The users file has lines of the form username:password */ protected String getPassword(String username) throws IOException { File f = new File(pwfile); if (!f.exists()) { logger.log(Level.SEVERE,"Don't see password file " + pwfile); } BufferedReader reader = new BufferedReader(new FileReader(pwfile)); String line = null; while ((line = reader.readLine()) != null ) { String pieces[] = line.split("\\s*\\:\\s*"); if (pieces[0].equals(username)) { reader.close(); return pieces[1]; } } reader.close(); return null; } /** returns the set of all known usernames */ protected Set<String> getUserNames() throws IOException { HashSet<String> output = new HashSet<String>(); BufferedReader reader = new BufferedReader(new FileReader(pwfile)); String line = null; while ((line = reader.readLine()) != null ) { String pieces[] = line.split("\\s*\\:\\s*"); output.add(pieces[0]); } return output; } }