/* GanymedeServer.java The GanymedeServer object is created by the arlut.csd.ganymede.server.Ganymede class at start-up time and published to the net for client logins via RMI. As such, the GanymedeServer object is the first Ganymede code that a client will directly interact with. Created: 17 January 1997 Module By: Jonathan Abbey, jonabbey@arlut.utexas.edu ----------------------------------------------------------------------- Ganymede Directory Management System Copyright (C) 1996-2014 The University of Texas at Austin Ganymede is a registered trademark of The University of Texas at Austin Contact information Web site: http://www.arlut.utexas.edu/gash2 Author Email: ganymede_author@arlut.utexas.edu Email mailing list: ganymede@arlut.utexas.edu US Mail: Computer Science Division Applied Research Laboratories The University of Texas at Austin PO Box 8029, Austin TX 78713-8029 Telephone: (512) 835-3200 This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see <http://www.gnu.org/licenses/>. */ package arlut.csd.ganymede.server; import java.io.BufferedReader; import java.io.File; import java.io.FileNotFoundException; import java.io.FileReader; import java.io.IOException; import java.rmi.RemoteException; import java.rmi.server.ServerNotActiveException; import java.rmi.server.UnicastRemoteObject; import java.util.Date; import java.util.Hashtable; import java.util.Vector; import arlut.csd.Util.TranslationService; import arlut.csd.ganymede.common.AdminEntry; import arlut.csd.ganymede.common.ClientMessage; import arlut.csd.ganymede.common.ErrorTypeEnum; import arlut.csd.ganymede.common.Invid; import arlut.csd.ganymede.common.NotLoggedInException; import arlut.csd.ganymede.common.Query; import arlut.csd.ganymede.common.QueryDataNode; import arlut.csd.ganymede.common.QueryNode; import arlut.csd.ganymede.common.Result; import arlut.csd.ganymede.common.ReturnVal; import arlut.csd.ganymede.common.SchemaConstants; import arlut.csd.ganymede.rmi.Server; import arlut.csd.ganymede.rmi.adminSession; /*------------------------------------------------------------------------------ class GanymedeServer ------------------------------------------------------------------------------*/ /** * <p>The GanymedeServer object is created by the * {@link arlut.csd.ganymede.server.Ganymede Ganymede class} at start-up time * and published to the net for client logins via RMI. As such, * the GanymedeServer object is the first Ganymede code that a client * will directly interact with.</p> */ public final class GanymedeServer implements Server { /** * <p>TranslationService object for handling string localization in * the Ganymede server.</p> */ static final TranslationService ts = TranslationService.getTranslationService("arlut.csd.ganymede.server.GanymedeServer"); /** * <p>Singleton server object. A running Ganymede Server will have one * instance of GanymedeServer active and bound into the RMI registry, * and this field will point to it.</p> */ static GanymedeServer server = null; /** * <p>You may want to change this if you don't want to let the * monitor account do unprivileged (Default role equivalent) queries * on the client.</p> */ static private final boolean ALLOW_MONITOR_CLIENT_USE = true; /** * <p>Vector of {@link arlut.csd.ganymede.server.GanymedeSession * GanymedeSession} objects for user sessions to be monitored by the * admin console.</p> * * <p>Note that there may be GanymedeSession objects active that are * not listed in this sessions Vector; GanymedeSession objects used for * server-side internal operations are not counted here. This Vector is * primarily used to keep track of things for the admin console code in * {@link arlut.csd.ganymede.server.GanymedeAdmin GanymedeAdmin}.</p> */ static private Vector<GanymedeSession> userSessions = new Vector<GanymedeSession>(); /** * <p>A hashtable mapping session names to identity. Used by the * login process to insure that each active session will be given a * unique identifying name.</p> * * <p>Will hold a superset of the keys in the activeUserSessionNames * hash unless code is operating within a synchronized block on this * hash.</p> */ static private Hashtable<String, String> activeSessionNames = new Hashtable<String, String>(); /** * <p>A hashtable mapping user session names to identity. Used by * the login process to insure that each active user session will be * given a unique identifying name.</p> */ static private Hashtable<String, String> activeUserSessionNames = new Hashtable<String, String>(); /** * <p>A hashtable mapping user Invids to a java.lang.Date object * representing the last time that user logged into Ganymede. This * data structure is used to check to see if the server's motd.html * file has changed since the user last logged in. This hash of * timestamps is not preserved in the ganymede.db file, so whenever * the server is restarted, all users are presumed to need to see * the motd.html file on their next login.</p> */ static private Hashtable<Invid, Date> userLogOuts = new Hashtable<Invid, Date>(); /** * <p>If true, the server is waiting for all users to disconnect * so that it can shut itself down.</p> */ static boolean shutdown = false; /** * <p>Our handy, all purpose counting semaphore for managing user * sessions.</p> */ static loginSemaphore lSemaphore = new loginSemaphore(); /** * <p>Another handy semaphore, this time used to defer * shutdowns from build processes</p> */ static loginSemaphore shutdownSemaphore = new loginSemaphore(); /** * <p>Message string explaining reason for shut down. Displayed to * connected users and/or users blocked from logging in while we're * waiting to shut down.</p> */ static private String shutdownReason = null; /** * <p>If the server was ordered to shut down by an admin on the * admin console or via the stopServer command line tool, record the * admin's identity here.</p> */ static private GanymedeAdmin shutdownAdmin = null; /** * <p>During the login process, we need to get exclusive access over * an extended time to synchronized methods in a privileged * GanymedeSession to do the query operations for login. If we used * the generic Ganymede.internalSession for this, we might lock the * server up, as Ganymede.internalSession is also used for Invid * label look up operations in the transaction commit process, which * involves a writeLock that will block the login's read lock from * being granted.</p> * * <p>By having our own private GanymedeSession for logins, we avoid * this deadlock possibility.</p> */ private GanymedeSession loginSession; /* -- */ /** * GanymedeServer constructor. We only want one server running * per invocation of Ganymede, so we'll check that here. */ public GanymedeServer() throws RemoteException { synchronized (GanymedeServer.class) { if (server == null) { server = this; } else { Ganymede.debug(ts.l("init.multiserver")); throw new RemoteException(ts.l("init.multiserver")); } } loginSession = new GanymedeSession(); // supergash Ganymede.rmi.publishObject(this); } /** * <p>Simple RMI ping test method.. this method is here so that the * {@link arlut.csd.ganymede.client.ClientBase ClientBase} class can * test to see whether it has truly gotten a valid RMI reference to * the server.</p> * * @see arlut.csd.ganymede.rmi.Server */ public boolean up() throws RemoteException { return true; } /** * <p>Client login method. Establishes a {@link * arlut.csd.ganymede.server.GanymedeSession GanymedeSession} object in the * server for the client, and returns a serializable {@link * arlut.csd.ganymede.common.ReturnVal ReturnVal} object which will contain * a {@link arlut.csd.ganymede.rmi.Session Session} remote reference * for the client to use, if login was successful.</p> * * <p>If login is not successful, the ReturnVal object will encode * a failure condition, along with a dialog explaining the problem.</p> * * <p>The GanymedeSession object contains all of the server's * knowledge about a given client's status., and is tracked by * the GanymedeServer object for statistics and for the admin * console's monitoring support.</p> * * @see arlut.csd.ganymede.rmi.Server */ public ReturnVal login(String username, String password) throws RemoteException { return processLogin(username, password, true, true); } /** * <p>XML Client login method. Establishes a {@link * arlut.csd.ganymede.server.GanymedeXMLSession GanymedeXMLSession} object * in the server for the client, and returns a serializable {@link * arlut.csd.ganymede.common.ReturnVal ReturnVal} object which will contain * a {@link arlut.csd.ganymede.rmi.XMLSession XMLSession} remote reference * for the client to use, if login was successful.</p> * * <p>If login is not successful, the ReturnVal object will encode * a failure condition, along with a dialog explaining the problem.</p> * * <p>The GanymedeXMLSession object in turn contains a * GanymedeSession object, which contains all of the server's * knowledge about a given client's status., and is tracked by the * GanymedeServer object for statistics and for the admin console's * monitoring support.</p> * * @see arlut.csd.ganymede.rmi.Server */ public ReturnVal xmlLogin(String username, String password) throws RemoteException { ReturnVal retVal = processLogin(username, password, true, false); if (!retVal.didSucceed()) // XXX processLogin never returns null { return retVal; } GanymedeSession mySession = (GanymedeSession) retVal.getSession(); GanymedeXMLSession xSession = new GanymedeXMLSession(mySession); // spawn the GanymedeXMLSession's background parser thread xSession.start(); // publish the GanymedeXMLSession for the client to use Ganymede.rmi.publishObject(xSession); // replace our remote Session reference with the XMLSession // reference, and pass it to the client.. they can query the // XMLSession for the Session reference if they need to. retVal.setXMLSession(xSession); return retVal; } /** * <p>This internal method handles the client login logic for both * the normal interactive client and the xml batch client.</p> * * @param clientName The user/persona name to be logged in * @param clientPass The password (in plaintext) to authenticate with * @param directSession If true, the GanymedeSession returned will * be published for remote RMI access. * @param exportObjects If true, the DBObjects viewed and edited by * the GanymedeSession will be exported for remote RMI access. */ private ReturnVal processLogin(String clientName, String clientPass, boolean directSession, boolean exportObjects) throws RemoteException { ReturnVal semaphoreResult = incrementAndTestLoginSemaphore(); if (!ReturnVal.didSucceed(semaphoreResult)) { return semaphoreResult; } boolean success = false; try { DBObject userOrAdminObj = validateUserOrAdminLogin(clientName, clientPass); if (userOrAdminObj == null) { return reportFailedLogin(clientName); } DBObject personaObj = null; DBObject userObj = null; if (userOrAdminObj.getTypeID() == SchemaConstants.PersonaBase) { personaObj = userOrAdminObj; userObj = getUserFromPersona(personaObj); clientName = personaObj.getLabel(); // canonicalize } else if (userOrAdminObj.getTypeID() == SchemaConstants.UserBase) { userObj = userOrAdminObj; clientName = userObj.getLabel(); } GanymedeSession session = new GanymedeSession(GanymedeServer.registerUserSessionName(clientName), userObj, personaObj, directSession, exportObjects); monitorUserSession(session); success = true; return reportSuccessLogin(session); } catch (Throwable ex) { Ganymede.logError(ex, ts.l("processLogin.failure")); return reportFailedLogin(clientName); } finally { if (!success) { // notify the consoles after decrementing the login // semaphore so the notify won't show the transient // semaphore increment GanymedeServer.lSemaphore.decrement(); // "Bad login attempt for username: {0} from host {1}" Ganymede.debug(ts.l("reportFailedLogin.badlogevent", clientName, GanymedeServer.getClientHost())); } } } /** * <p>Returns null if we were able to increment the login semaphore, * or a ReturnVal encoding the problem if not.</p> */ private ReturnVal incrementAndTestLoginSemaphore() { String error = GanymedeServer.lSemaphore.increment(); if ("shutdown".equals(error)) { if (shutdownReason != null) { // "No logins allowed" // "The server is currently waiting to shut down. No // logins will be accepted until the server has // restarted.\n\nReason for shutdown: {0}" return Ganymede.createErrorDialog(ts.l("incrementAndTestLoginSemaphore.nologins"), ts.l("incrementAndTestLoginSemaphore.nologins_shutdown_reason", shutdownReason)); } else { // "No logins allowed" // "The server is currently waiting to shut down. No // logins will be accepted until the server has // restarted." return Ganymede.createErrorDialog(ts.l("incrementAndTestLoginSemaphore.nologins"), ts.l("incrementAndTestLoginSemaphore.nologins_shutdown")); } } else if (error != null && error.startsWith("schema edit:")) { String adminName = error.substring("schema edit:".length()); // "No logins allowed" // "Logins to the Ganymede server are temporarily unavailable. // Admin {0} is editing the server''s schema definition." return Ganymede.createErrorDialog(ts.l("incrementAndTestLoginSemaphore.nologins"), ts.l("incrementAndTestLoginSemaphore.nologins_schema_edit", adminName)); } else if (error != null) { // "No logins allowed" // "Can''t log in to the Ganymede server.. semaphore disabled: {0}" return Ganymede.createErrorDialog(ts.l("incrementAndTestLoginSemaphore.nologins"), ts.l("incrementAndTestLoginSemaphore.nologins_semaphore", error)); } return null; } /** * <p>Logs and reports a failed login for user / admin clientName * coming from host clientHost.</p> * * <p>Returns a ReturnVal to pass back to the client describing the * failure.</p> */ private ReturnVal reportFailedLogin(String clientName) { if (Ganymede.log != null) { // "Bad login attempt for username: {0} from host {1}" Ganymede.log.logSystemEvent(new DBLogEvent("badpass", ts.l("reportFailedLogin.badlogevent", clientName, GanymedeServer.getClientHost()), null, clientName, null, null)); } // "Bad login attempt" // "Bad username or password, login rejected." ReturnVal badCredsRetVal = Ganymede.createErrorDialog(ts.l("reportFailedLogin.badlogin"), ts.l("reportFailedLogin.badlogintext")); badCredsRetVal.setErrorType(ErrorTypeEnum.BADCREDS); return badCredsRetVal; } /** * <p>Logs the successful login of a user and returns a ReturnVal * that includes a remote reference to the newly created * GanymedeSession.</p> */ private ReturnVal reportSuccessLogin(GanymedeSession session) { // "{0} logged in from {1}" Ganymede.debug(ts.l("reportSuccessLogin.loggedin", session.getIdentity(), session.getClientHostName())); if (Ganymede.log != null) { Vector<Invid> objects = new Vector<Invid>(); objects.add(session.getIdentityInvid()); // "OK login for username: {0} from host {1}" Ganymede.log.logSystemEvent(new DBLogEvent("normallogin", ts.l("reportSuccessLogin.logevent", session.getIdentity(), session.getClientHostName()), session.getIdentityInvid(), session.getIdentity(), objects, null)); } ReturnVal retVal = ReturnVal.success(); retVal.setSession(session); return retVal; } /** * <p>This method is called by the {@link * arlut.csd.ganymede.server.timeOutTask timeOutTask} scheduled * task, and forces an idle time check on any users logged in.</p> */ public void clearIdleSessions() { // clone the sessions Vector so any forceOff() resulting from a // timeCheck() call won't disturb the loop, and so that we won't // have to synchronize on sessions and risk nested monitor // deadlock Vector<GanymedeSession> sessionsCopy = new Vector<GanymedeSession>(userSessions); for (GanymedeSession session: sessionsCopy) { session.timeCheck(); } } /** * <p>This method is called by the admin console via the {@link * arlut.csd.ganymede.server.GanymedeAdmin GanymedeAdmin} class to * kick all user sessions off of the server.</p> */ public void killAllUsers(String reason) { // clone the sessions Vector so any forceOff() won't disturb the // loop Vector<GanymedeSession> sessionsCopy = new Vector<GanymedeSession>(userSessions); for (GanymedeSession session: sessionsCopy) { session.forceOff(reason); } } /** * <p>This method is called by the admin console via the {@link * arlut.csd.ganymede.server.GanymedeAdmin GanymedeAdmin} class to * kick a specific user session off of the server.</p> */ public boolean killUser(String username, String reason) { // it's okay to loop inside sessions since we'll exit the loop as // soon as we do a forceOff (which can cause the sessions Vector // to be modified in a way that would otherwise disturb the loop synchronized (userSessions) { for (GanymedeSession session: userSessions) { if (session.getSessionName().equals(username)) { session.forceOff(reason); return true; } } } return false; } /** * <p>This public remotely accessible method is called by the * Ganymede admin console and/or the Ganymede stopServer script to * establish a new admin console connection to the server. * Establishes an GanymedeAdmin object in the server.</p> * * <p>Adds <admin> as a monitoring admin console.</p> * * @see arlut.csd.ganymede.rmi.Server */ public ReturnVal admin(String clientName, String clientPass) throws RemoteException { String clientHost = GanymedeServer.getClientHost(); String error = GanymedeServer.lSemaphore.checkEnabled(); if (error != null) { // "Admin Console Connect Failure" // "Can''t connect admin console to server.. semaphore disabled: {0}" return Ganymede.createErrorDialog(ts.l("admin.connect_failure"), ts.l("admin.semaphore_failure", error)); } DBObject adminObj = validateAdminLogin(clientName, clientPass); int validationResult = validateConsoleAdminPersona(adminObj); if (validationResult == 0) { if (Ganymede.log != null) { // "Bad console attach attempt by: {0} from host {1}" Ganymede.log.logSystemEvent(new DBLogEvent("badpass", ts.l("admin.badlogevent", clientName, clientHost), null, clientName, null, null)); } // "Bad console attach attempt by: {0} from host {1}" Ganymede.debug(ts.l("admin.badlogevent", clientName, clientHost)); // "Login Failure" // "Bad username and/or password for admin console" return Ganymede.createErrorDialog(ts.l("admin.badlogin"), ts.l("admin.baduserpass")); } adminSession aSession = new GanymedeAdmin(validationResult >= 2, adminObj.getInvid(), clientName, clientHost); // now Ganymede.debug() will write to the newly attached console, // even though we haven't returned the admin session to the admin // console client String eventStr = ts.l("admin.goodlogevent", clientName, clientHost); Ganymede.debug(eventStr); if (Ganymede.log != null) { Ganymede.log.logSystemEvent(new DBLogEvent("adminconnect", eventStr, null, clientName, null, null)); } ReturnVal retVal = new ReturnVal(true); retVal.setAdminSession(aSession); return retVal; } /** * <p>Returns the user linked to the provided personaObj, or null if * no user is attached.</p> */ public DBObject getUserFromPersona(DBObject personaObj) { if (personaObj.getTypeID() != SchemaConstants.PersonaBase) { throw new IllegalArgumentException(); } Invid userInvid = (Invid) personaObj.getFieldValueLocal(SchemaConstants.PersonaAssocUser); if (userInvid == null) { return null; } return loginSession.getDBSession().viewDBObject(userInvid); } /** * <p>Returns a user or admin persona DBObject if the given * user/admin name exists, and has the given password in the * database, and is permitted to login and have a * GanymedeSession.</p> * * <p>Returns null if no such user or admin persona / password pair * exists.</p> */ public DBObject validateUserOrAdminLogin(String name, String clientPass) { DBObject personaObj = this.validateAdminLogin(name, clientPass); if (personaObj == null) { return this.validateUserLogin(name, clientPass); } if (!ALLOW_MONITOR_CLIENT_USE) { if (personaObj.getInvid().equals(Invid.createInvid(SchemaConstants.PersonaBase, SchemaConstants.PersonaMonitorObj))) { return null; } } DBObject userObj = getUserFromPersona(personaObj); if (userObj != null && userObj.isInactivated()) { return null; } return personaObj; } /** * <p>Returns a user DBObject if the given user name has the given * password in the database.</p> * * <p>Returns null if no such user / password pair exists.</p> */ public DBObject validateUserLogin(String userName, String clientPass) { Query userQuery = new Query(SchemaConstants.UserBase, new QueryDataNode(SchemaConstants.UserUserName, QueryDataNode.NOCASEEQ, userName), false); Result result = loginSession.internalSingletonQuery(userQuery); if (result != null) { DBObject user = loginSession.getDBSession().viewDBObject(result.getInvid()); PasswordDBField pdbf = user.getPassField(SchemaConstants.UserPassword); if (pdbf != null && pdbf.matchPlainText(clientPass)) { return user; } } return null; } /** * <p>Returns an admin persona DBObject if the given personaName * exists and has the given password in the database.</p> * * <p>Returns null if no such admin persona / password pair * exists.</p> */ public DBObject validateAdminLogin(String personaName, String clientPass) { Query adminQuery = new Query(SchemaConstants.PersonaBase, new QueryDataNode(SchemaConstants.PersonaLabelField, QueryDataNode.NOCASEEQ, personaName), false); Result result = loginSession.internalSingletonQuery(adminQuery); if (result != null) { DBObject personaObj = loginSession.getDBSession().viewDBObject(result.getInvid()); PasswordDBField pdbf = personaObj.getPassField(SchemaConstants.PersonaPasswordField); if (pdbf != null && pdbf.matchPlainText(clientPass)) { return personaObj; } } return null; } /** * This method determines whether the specified username/password * combination is valid for an admin persona. * * @param adminObj DBObject corresponding to an admin object to * check for console privs. * @return 0 if the admin doesn't have admin console privileges, * 1 the admin is allowed basic admin console access, * 2 the admin is allowed full admin console privileges, * 3 the admin is allowed interpreter access. */ public int validateConsoleAdminPersona(DBObject adminObj) { if (adminObj == null) { return 0; } if (adminObj.getTypeID() != SchemaConstants.PersonaBase) { throw new RuntimeException("Invalid object type"); } // Are we the One True Amazing Supergash Root User Person? He // gets full privileges by default. if (adminObj.getInvid().equals(Invid.createInvid(SchemaConstants.PersonaBase, SchemaConstants.PersonaSupergashObj))) { return 3; } else { // Is this user prohibited from accessing the admin // console? if (!adminObj.isSet(SchemaConstants.PersonaAdminConsole)) { return 0; } // Ok, they can access the admin console...but do they // have full privileges? if (!adminObj.isSet(SchemaConstants.PersonaAdminPower)) { return 1; } // Ok, they have full privileges...but can they access the // admin interpreter? if (!adminObj.isSet(SchemaConstants.PersonaInterpreterPower)) { return 2; } return 3; } } /** ------------------------------------------------------------------------------ Static Methods ------------------------------------------------------------------------------ */ /** * <p>This method is used by GanymedeSession.login() to find and * record a unique name for an internal session. It is matched with * clearSession(), below.</p> */ static String registerInternalSessionName(String sessionName) { if (sessionName == null) { throw new IllegalArgumentException("invalid null sessionName"); } String temp = sessionName; int i = 2; synchronized (activeSessionNames) { while (activeSessionNames.containsKey(sessionName)) { sessionName = temp + "[" + i + "]"; i++; } activeSessionNames.put(sessionName, sessionName); } return sessionName; } /** * <p>This method is used by GanymedeSession.login() to find and * record a unique name for a user session. It is matched with * clearSession(), below.</p> */ static String registerUserSessionName(String sessionName) { if (sessionName == null) { throw new IllegalArgumentException("invalid null sessionName"); } String temp = sessionName; int i = 2; synchronized (activeSessionNames) { while (activeSessionNames.contains(sessionName)) { sessionName = temp + "[" + i + "]"; i++; } activeSessionNames.put(sessionName, sessionName); activeUserSessionNames.put(sessionName, sessionName); } return sessionName; } /** * <p>This method handles clearing a remote session's session name * from the activeSessionNames and activeRemoteSessionNames * hashes.</p> * * <p>If this server is in deferred shutdown mode and this is the * last logged in remote session, we'll proceed to shut-down.</p> * * <p>Note that the session parameter must not have cleared its * permManager reference before calling this method.</p> */ static void clearSession(GanymedeSession session) { boolean userSession = false; Invid userInvid = session.getPermManager().getUserInvid(); String sessionName = session.getPermManager().getSessionName(); if (userInvid != null) { userLogOuts.put(userInvid, new Date()); } synchronized (activeSessionNames) { activeSessionNames.remove(sessionName); if (activeUserSessionNames.remove(sessionName) != null) { userSession = true; // if we are in deferred shutdown mode and this was the last // remote user logged in, spin off a thread to shut the server // down if (shutdown && activeUserSessionNames.size() == 0) { Thread deathThread = new Thread(new Runnable() { public void run() { // sleep for 5 seconds to let our last client disconnect try { java.lang.Thread.currentThread().sleep(5000); } catch (InterruptedException ex) { } GanymedeServer.shutdown(); } }, ts.l("clearActiveUser.deathThread")); deathThread.start(); } } } if (session.isUserSession()) { try { GanymedeServer.lSemaphore.decrement(); } catch (IllegalArgumentException ex) { Ganymede.logError(ex); } } if (userSession) { unmonitorUserSession(session); } } /** * <p>This method is used by the {@link * arlut.csd.ganymede.server.GanymedeAdmin#shutdown(boolean, * java.lang.String) shutdown()} method to put the server into * 'shutdown soon' mode.</p> */ public static void setShutdown(String reason, GanymedeAdmin adminConsole) { if (reason != null) { GanymedeServer.shutdownReason = reason; } if (adminConsole != null) { GanymedeServer.shutdownAdmin = adminConsole; } // turn off the login semaphore. this will block any new clients // or admin consoles from connecting while we shut down try { GanymedeServer.lSemaphore.disable("shutdown", false, 0); } catch (InterruptedException ex) { Ganymede.logError(ex); throw new RuntimeException(ex.getMessage()); } // if no one is logged in, right now, shut er down. if (GanymedeServer.lSemaphore.getCount() == 0) { GanymedeAdmin.setState(ts.l("setShutDown.nousers_state")); GanymedeServer.shutdown(); return; } // otherwise by setting the shutdown variable to true, we signal // clearActiveUser() to shut us down if the remote client count // drops to 0 shutdown = true; GanymedeAdmin.setState(ts.l("setShutDown.waiting_state")); } /** * <p>Shut down the server without changing any previously set * shutdownReason or shutdownAdmin identifier.</p> */ public static ReturnVal shutdown() { return GanymedeServer.shutdown(null, null); } /** * <p>This method actually does the shutdown.</p> */ public static ReturnVal shutdown(String reason, GanymedeAdmin adminConsole) { if (reason != null) { GanymedeServer.shutdownReason = reason; } if (adminConsole != null) { GanymedeServer.shutdownAdmin = adminConsole; } String semaphoreState = GanymedeServer.lSemaphore.checkEnabled(); if (!"shutdown".equals(semaphoreState)) { // turn off the login semaphore. this will block any new clients or admin consoles // from connecting while we shut down try { semaphoreState = GanymedeServer.lSemaphore.disable("shutdown", false, 0); // no blocking } catch (InterruptedException ex) { Ganymede.logError(ex); throw new RuntimeException(ex.getMessage()); } if (semaphoreState != null) { return Ganymede.createErrorDialog(ts.l("shutdown.failure"), ts.l("shutdown.failure_text", semaphoreState)); } } String shuttingDownNowMsg = null; if (GanymedeServer.shutdownReason == null || GanymedeServer.shutdownReason.trim().equals("")) { if (GanymedeServer.shutdownAdmin == null) { // "Server going down" shuttingDownNowMsg = ts.l("shutdown.clientNotification"); } else { // "Ganymede admin {0} on host {1} shutting down server." shuttingDownNowMsg = ts.l("shutdown.console_notify_admin", shutdownAdmin.getAdminName(), shutdownAdmin.getAdminHost()); } } else { if (GanymedeServer.shutdownAdmin == null) { // "Ganymede server going down:\n{0}" shuttingDownNowMsg = ts.l("shutdown.console_notify_reason", GanymedeServer.shutdownReason); } else { // "Ganymede admin {1} on host {2} shutting down server:\n\n{0}" shuttingDownNowMsg = ts.l("shutdown.console_notify_reason_admin", GanymedeServer.shutdownReason, shutdownAdmin.getAdminName(), shutdownAdmin.getAdminHost()); } } Ganymede.debug(shuttingDownNowMsg); // "Server going down.. waiting for any builder tasks to finish phase 2" Ganymede.debug(ts.l("shutdown.goingdown")); try { shutdownSemaphore.disable("shutdown", true, -1); } catch (InterruptedException ex) { // not much that we can do at this point } // at this point, no new builder tasks can be scheduled // "Server going down.. performing final dump" Ganymede.debug(ts.l("shutdown.dumping")); try { // from this point on, we will go down, no matter what // exceptions might percolate up to this point // dump, then shut down. This dump call will cause us to // block until all write locks queued up are processed and // released. Our second dump parameter is false, so that we // are guaranteed that no internal client can get a writelock // and maybe get a transaction off that would cause us // confusion. try { Ganymede.db.dump(Ganymede.dbFilename, false, false); // don't release lock, don't archive last } catch (IOException ex) { // "shutdown error: couldn''t successfully consolidate db." Ganymede.debug(ts.l("shutdown.dumperror")); throw ex; // maybe didn't lock, so go down hard } // ok, we now are left holding a dump lock. it should be safe to kick // everybody off and shut down the server // "Server going down.. database locked" Ganymede.debug(ts.l("shutdown.locked")); // "Server going down.. disconnecting clients" Ganymede.debug(ts.l("shutdown.clients")); // forceOff modifies GanymedeServer.userSessions, so we need // to copy our list before we iterate over it. Vector<GanymedeSession> tempList = new Vector<GanymedeSession>(userSessions); for (GanymedeSession temp: tempList) { temp.forceOff(shuttingDownNowMsg); } // "Server going down.. interrupting scheduler" Ganymede.debug(ts.l("shutdown.scheduler")); Ganymede.scheduler.interrupt(); // "Server going down.. disconnecting consoles" Ganymede.debug(ts.l("shutdown.consoles")); // "Server going down now." GanymedeAdmin.closeAllConsoles(ts.l("shutdown.byeconsoles")); // disconnect the Jython server /* Ganymede.debug(ts.l("shutdown.jython")); Ganymede.jythonServer.shutdown(); */ // log our shutdown and close the log if (Ganymede.log != null) { if (shutdownAdmin == null) { Ganymede.log.logSystemEvent(new DBLogEvent("shutdown", shuttingDownNowMsg, null, null, null, null)); } else { Ganymede.log.logSystemEvent(new DBLogEvent("shutdown", shuttingDownNowMsg, shutdownAdmin.getAdminInvid(), null, null, null)); } System.err.println(); // "Server completing shutdown.. waiting for log thread to complete." System.err.println(ts.l("shutdown.closinglog")); try { Ganymede.log.close(); // this will block until the mail queue drains } catch (IOException ex) { System.err.println(ts.l("shutdown.logIOException", ex.toString())); } } } catch (Exception ex) { // "Caught exception during final shutdown:" Ganymede.logError(ex, ts.l("shutdown.Exception")); } catch (Error ex) { // "Caught error during final shutdown:" Ganymede.logError(ex, ts.l("shutdown.Error")); } finally { System.err.println(); // "Server shutdown complete." System.err.println(ts.l("shutdown.finally")); arlut.csd.ganymede.common.Invid.printCount(); if (Ganymede.signalHandlingThread != null) { java.lang.Runtime.getRuntime().removeShutdownHook(Ganymede.signalHandlingThread); } System.exit(0); return null; } } /** * <p>Returns the name of the client that is doing an RMI call on * the server.</p> * * <p>Will return "unknown" if the thread that calls this method * wasn't initiated by an RMI call.</p> */ public static String getClientHost() { try { String ipAddress = UnicastRemoteObject.getClientHost(); try { java.net.InetAddress addr = java.net.InetAddress.getByName(ipAddress); return addr.getHostName(); } catch (java.net.UnknownHostException ex) { return ipAddress; } } catch (ServerNotActiveException ex) { return "unknown"; } } /** * <p>This method is triggered from the admin console when the user * runs an 'Invid Sweep'. It is designed to scan through the * Ganymede datastore's reference fields and clean out any * references found that point to non-existent objects.</p> * * @return true if there were any invalid invids in the database */ public boolean sweepInvids() { boolean swept = false; // XXX // // it's safe to use Ganymede.internalSession's DBSession here only // because we don't call the synchronized viewDBObject method on // it unless and until we are granted the DBDumpLock, and because // we are not a synchronized method on GanymedeServer. // // XXX DBSession session = Ganymede.internalSession.getDBSession(); /* -- */ // make sure we're ok to sweep DBDumpLock lock = new DBDumpLock(Ganymede.db); try { lock.establish("sweepInvids"); // wait until we get our lock } catch (InterruptedException ex) { Ganymede.debug(ts.l("sweepInvids.noproceed")); return false; // actually we just failed, but same difference } try { for (DBObjectBase base: Ganymede.db.bases()) { Ganymede.debug(ts.l("sweepInvids.sweeping", base.toString())); for (DBObject object: base.getObjects()) { // loop 3: iterate over the fields present in this object for (DBField field: object.getFieldVect()) { if (field == null || !(field instanceof InvidDBField)) { continue; // only check invid fields } InvidDBField iField = (InvidDBField) field; if (iField.isVector()) { Vector<Invid> tempVector = (Vector<Invid>) iField.getVectVal(); // clear out the invid's held in this field pending // successful lookup iField.value = new Vector(); for (Invid invid: tempVector) { if (session.viewDBObject(invid) != null) { iField.getVectVal().add(invid); // keep this invid } else { Ganymede.debug(ts.l("sweepInvids.removing_vector", invid.toString(), iField.getName(), base.getName(), object.getLabel())); swept = true; } } } else { Invid invid = (Invid) iField.value; if (session.viewDBObject(invid) == null) { swept = true; Ganymede.debug(ts.l("sweepInvids.removing_scalar", invid.toString(), iField.getName(), base.getName(), object.getLabel())); } } } } } } finally { lock.release(); } Ganymede.debug(ts.l("sweepInvids.done")); return swept; } /** * <p>This method is used for testing. This method sweeps through * all invid's listed in the (loaded) database, and checks to make * sure that they all point to valid objects in the datastore. * Invid fields that are in symmetric relationships are tested to * make sure both ends of the symmetry properly hold.</p> * * @return true if there were any broken invids in the database */ public boolean checkInvids() { boolean ok = true; // XXX // // it's safe to use Ganymede.internalSession's DBSession here only // because we don't call the synchronized viewDBObject method on // it unless and until we are granted the DBDumpLock, and because // we are not a synchronized method on GanymedeServer. // // XXX DBSession session = Ganymede.internalSession.getDBSession(); /* -- */ DBDumpLock lock = new DBDumpLock(Ganymede.db); try { lock.establish("checkInvids"); // wait until we get our lock } catch (InterruptedException ex) { Ganymede.debug(ts.l("checkInvids.noproceed")); return false; // actually we just failed, but same difference } try { // first we're going to do our forward test, making sure that // all pointers registered in the objects in our data store // point to valid objects and that they have valid symmetric // back pointers or virtual back pointer registrations in the // DBLinkTracker class. for (DBObjectBase base: Ganymede.db.bases()) { Ganymede.debug(ts.l("checkInvids.checking", base.getName())); for (DBObject object: base.getObjects()) { for (DBField field: object.getFieldVect()) { // we only care about invid fields if (field == null || !(field instanceof InvidDBField)) { continue; } InvidDBField iField = (InvidDBField) field; if (!iField.test(session, (base.getName() + ":" + object.getLabel()))) { ok = false; } } } } // validate the backPointers structure that we use to quickly // find objects pointing to other objects with non-symmetric // links if (!Ganymede.db.aSymLinkTracker.checkInvids(session)) { ok = false; } } finally { lock.release(); } Ganymede.debug(ts.l("checkInvids.done")); return ok; } /** * <p>This method is used for testing. This method sweeps * through all embedded objects in the (loaded) database, and * checks to make sure that they all have valid containing objects.</p> * * @return true if there were any embedded objects without containers in * the database */ public boolean checkEmbeddedObjects() { boolean ok = true; // XXX // // it's safe to use Ganymede.internalSession's DBSession here only // because we don't call the synchronized viewDBObject method on // it unless and until we are granted the DBDumpLock, and because // we are not a synchronized method on GanymedeServer. // // XXX GanymedeSession gSession = Ganymede.internalSession; /* -- */ DBDumpLock lock = new DBDumpLock(Ganymede.db); try { lock.establish("checkEmbeddedObjects"); // wait until we get our lock } catch (InterruptedException ex) { Ganymede.debug(ts.l("checkEmbeddedObjects.noproceed")); return false; // actually we just failed, but same difference } try { // loop over the object bases for (DBObjectBase base: Ganymede.db.bases()) { if (!base.isEmbedded()) { continue; } // loop over the objects in this base Ganymede.debug(ts.l("checkEmbeddedObjects.checking", base.getName())); for (DBObject object: base.getObjects()) { try { gSession.getContainingObj(object); } catch (IntegrityConstraintException ex) { Ganymede.debug(ts.l("checkEmbeddedObjects.aha", object.getTypeName(), object.getLabel())); ok = false; } } } } finally { lock.release(); } Ganymede.debug(ts.l("checkEmbeddedObjects.done")); return ok; } /** * <p>This method is used for fixing the server if it somehow leaks * embedded objects.. This method sweeps * through all embedded objects in the (loaded) database, and * deletes any that do not have valid containing objects.</p> */ public ReturnVal sweepEmbeddedObjects() { Vector<Invid> invidsToDelete = new Vector<Invid>(); // XXX // // it's safe to use Ganymede.internalSession's DBSession here only // because we don't call the synchronized viewDBObject method on // it unless and until we are granted the DBDumpLock, and because // we are not a synchronized method on GanymedeServer. // // XXX GanymedeSession gSession = Ganymede.internalSession; /* -- */ DBDumpLock lock = new DBDumpLock(Ganymede.db); try { lock.establish("checkEmbeddedObjects"); // wait until we get our lock } catch (InterruptedException ex) { return Ganymede.createErrorDialog(ts.l("sweepEmbeddedObjects.failure"), ts.l("sweepEmbeddedObjects.failure_text")); } try { for (DBObjectBase base: Ganymede.db.bases()) { if (!base.isEmbedded()) { continue; } // loop over the objects in this base Ganymede.debug(ts.l("sweepEmbeddedObjects.checking", base.getName())); for (DBObject object: base.getObjects()) { try { gSession.getContainingObj(object); } catch (IntegrityConstraintException ex) { invidsToDelete.add(object.getInvid()); } } } } finally { lock.release(); } if (invidsToDelete.size() == 0) { Ganymede.debug(ts.l("sweepEmbeddedObjects.complete")); return null; } // we want a private, supergash-privileged GanymedeSession try { gSession = new GanymedeSession(":embeddedSweep"); } catch (RemoteException ex) { Ganymede.logError(ex); throw new RuntimeException(ex.getMessage()); } try { // we're going to delete the objects by skipping the GanymedeSession // permission layer, which will break on non-contained embedded objects DBSession session = gSession.getDBSession(); // we want a non-interactive transaction.. if an object removal fails, the // whole transaction will fail, no rollbacks. gSession.openTransaction("embedded object sweep", false); // non-interactive for (Invid objInvid: invidsToDelete) { ReturnVal retVal = session.deleteDBObject(objInvid); if (!ReturnVal.didSucceed(retVal)) { // "Couldn''t delete object {0}" Ganymede.debug(ts.l("sweepEmbeddedObjects.delete_failure", gSession.getDBSession().getObjectLabel(objInvid))); } else { // "Deleted object {0}" Ganymede.debug(ts.l("sweepEmbeddedObjects.delete_ok", gSession.getDBSession().getObjectLabel(objInvid))); } } return gSession.commitTransaction(); } catch (NotLoggedInException ex) { return Ganymede.createErrorDialog(ts.l("sweepEmbeddedObjects.error"), ts.l("sweepEmbeddedObjects.error_text", ex.getMessage())); } finally { gSession.logout(); } } /** * <p>Handy public accessor for the login semaphore, for * possible use by plug-in task code.</p> */ public static loginSemaphore getLoginSemaphore() { return GanymedeServer.lSemaphore; } /** * <p>Gated enabled test. If this method returns null, logins are allowed * at the time checkEnabled() is called. This method is to be used by admin * consoles, which should not connect to the server during schema editing or * server shut down, but which should not affect the login count for reasons * of blocking a schema edit disable, say.</p> * * @return null if logins are currently enabled, or a message string if they * are disabled. */ public static String checkEnabled() { return GanymedeServer.lSemaphore.checkEnabled(); } /** * <p>This method is called to add a remote user's * {@link arlut.csd.ganymede.server.GanymedeSession GanymedeSession} * object to the GanymedeServer's static * {@link arlut.csd.ganymede.server.GanymedeServer#userSessions userSessions} * field, which is used by the admin console code to iterate * over connected users when logging user actions to the * Ganymede admin console.</p> */ public static void monitorUserSession(GanymedeSession session) { synchronized (userSessions) { sendMessageToRemoteSessions(ClientMessage.LOGIN, ts.l("addRemoteUser.logged_in", session.getUserName())); // we send the above message before adding the user to the // userSessions Vector so that the user doesn't get bothered // with a 'you logged in' message userSessions.add(session); sendMessageToRemoteSessions(ClientMessage.LOGINCOUNT, Integer.toString(userSessions.size())); } } /** * <p>This method is called to remove a remote user's * {@link arlut.csd.ganymede.server.GanymedeSession GanymedeSession} * object from the GanymedeServer's static * {@link arlut.csd.ganymede.server.GanymedeServer#userSessions userSessions} * field, which is used by the admin console code to iterate * over connected users when logging user actions to the * Ganymede admin console.</p> */ public static void unmonitorUserSession(GanymedeSession session) { synchronized (userSessions) { if (userSessions.remove(session)) { // we just removed the session of the user who logged out, so // they won't receive the log out message that we'll send to // the other clients sendMessageToRemoteSessions(ClientMessage.LOGOUT, ts.l("removeRemoteUser.logged_out", session.getUserName())); sendMessageToRemoteSessions(ClientMessage.LOGINCOUNT, Integer.toString(userSessions.size())); } } // update the admin consoles GanymedeAdmin.refreshUsers(); } /** * <p>This method is used by the * {@link arlut.csd.ganymede.server.GanymedeAdmin GanymedeAdmin} * refreshUsers() method to get a summary of the state of the * monitored user sessions.</p> */ public static Vector<AdminEntry> getUserTable() { Vector<AdminEntry> entries = null; synchronized (userSessions) { entries = new Vector<AdminEntry>(userSessions.size()); for (GanymedeSession session: userSessions) { if (session.isLoggedIn()) { entries.add(session.getAdminEntry()); } } } return entries; } /** * <p>Used by the Ganymede server to transmit notifications to * connected clients.</p> * * @param type Should be a valid value from arlut.csd.common.ClientMessage * @param message The message to send */ public static void sendMessageToRemoteSessions(int type, String message) { sendMessageToRemoteSessions(type, message, null); } /** * <p>Used by the Ganymede server to transmit notifications to * connected clients.</p> * * @param type Should be a valid value from arlut.csd.common.ClientMessage * @param message The message to send * @param self If non-null, sendMessageToRemoteSessions will skip sending the * message to self */ public static void sendMessageToRemoteSessions(int type, String message, GanymedeSession self) { Vector<GanymedeSession> sessionsCopy = new Vector<GanymedeSession>(userSessions); for (GanymedeSession session: sessionsCopy) { if (session != self) { session.sendMessage(type, message); } } } /** * <p>This method retrieves a message from a specified directory in * the Ganymede installation and passes it back as a StringBuffer. * Used by the Ganymede server to pass motd information to the * client.</p> * * @param key A text key indicating the file to be retrieved, minus * the .txt or .html extension * * @param userToDateCompare If not null, the Invid of a user on * whose behalf we want to retrieve the message. If the user has * logged in more recently than the timestamp of the file has * changed, we will return a null result * * @param html If true, return the .html version. If false, return * the .txt version. */ public static StringBuffer getTextMessage(String key, Invid userToDateCompare, boolean html) { if ((key.indexOf('/') != -1) || (key.indexOf('\\') != -1)) { throw new IllegalArgumentException(ts.l("getTextMessage.badargs")); } if (html) { key = key + ".html"; } else { key = key + ".txt"; } if (Ganymede.messageDirectoryProperty == null) { Ganymede.debug(ts.l("getTextMessage.nodir", key)); return null; } /* - */ String filename = arlut.csd.Util.PathComplete.completePath(Ganymede.messageDirectoryProperty) + key; File messageFile; /* -- */ messageFile = new File(filename); if (!messageFile.exists() || ! messageFile.isFile()) { return null; } if (userToDateCompare != null) { Date lastlogout = (Date) userLogOuts.get(userToDateCompare); if (lastlogout != null) { Date timestamp = new Date(messageFile.lastModified()); if (lastlogout.after(timestamp)) { return null; } } } // okay, read and copy! BufferedReader in = null; StringBuffer result = null; try { in = new BufferedReader(new FileReader(messageFile)); result = new StringBuffer(); try { String line = in.readLine(); while (line != null) { result.append(line); result.append("\n"); line = in.readLine(); } } catch (IOException ex) { Ganymede.debug(ts.l("getTextMessage.IOExceptionReport", filename, ex.getMessage())); Ganymede.debug(result.toString()); } } catch (FileNotFoundException ex) { Ganymede.debug("getTextMessage(" + key + "): FileNotFoundException"); return null; } finally { if (in != null) { try { in.close(); } catch (IOException ex) { } } } return result; } }