OSDN Git Service

Fix handling of H2 lock files
authorNick Clarke <memorius@gmail.com>
Wed, 21 Jul 2010 02:35:23 +0000 (14:35 +1200)
committerRandy Baumgarte <randy@fbn.cx>
Fri, 23 Jul 2010 09:31:06 +0000 (05:31 -0400)
We don't necessarily need to abort if an H2 db lock file exists.
H2 manages these lock files on its own, they should never need manual deleting;
see here for details:
http://www.h2database.com/html/advanced.html#file_locking_protocols

If the lock file exists and another process has the DB open,
H2 will automatically detect this and will throw an exception when trying to
open the DB. I've restructured some startup code to detect this and
cleanly abort NeverNote startup before we do anything dangerous like deleting
files from the 'res' dir. The correct resolution is to close or kill the other
NeverNote process, no need to do anything to the lock file.

If the lock file exists but no other process has the DB open, as happens if
NeverNote crashes badly e.g. due to a segfault in QTJambi, then H2 will also
detect this and will allow startup to proceed anyway.

src/cx/fbn/nevernote/NeverNote.java
src/cx/fbn/nevernote/config/FileManager.java
src/cx/fbn/nevernote/config/InitializationException.java
src/cx/fbn/nevernote/sql/DatabaseConnection.java
src/cx/fbn/nevernote/sql/requests/DatabaseRequest.java
src/cx/fbn/nevernote/sql/runners/RDatabaseConnection.java
src/cx/fbn/nevernote/threads/CounterRunner.java
src/cx/fbn/nevernote/threads/DBRunner.java
src/cx/fbn/nevernote/threads/IndexRunner.java
src/cx/fbn/nevernote/threads/SaveRunner.java
src/cx/fbn/nevernote/threads/SyncRunner.java

index 53c4035..c9a95b3 100644 (file)
@@ -291,33 +291,14 @@ public class NeverNote extends QMainWindow{
     //***************************************************************
     //***************************************************************
     // Application Constructor 
-       public NeverNote()  {
-                               
-               if (!lockApplication()) {
-                       System.exit(0);
-               }
+       private NeverNote(DatabaseConnection dbConn)  {
+               conn = dbConn;
+
                thread().setPriority(Thread.MAX_PRIORITY);
                
                logger = new ApplicationLogger("nevernote.log");
                logger.log(logger.HIGH, tr("Starting Application"));
-               conn = new DatabaseConnection(logger, Global.mainThreadId);
-               conn.dbSetup();
-               
-               logger.log(logger.EXTREME, tr("Creating index connection"));    
-               logger.log(logger.EXTREME, tr("Building DB thread"));
-               Global.dbRunnerSignal = new DBRunnerSignal();
-               if (Global.getDatabaseUrl().toUpperCase().indexOf("CIPHER=") > -1) {
-                       boolean goodCheck = false;
-                       while (!goodCheck) {
-                               DatabaseLoginDialog dialog = new DatabaseLoginDialog();
-                               dialog.exec();
-                               if (!dialog.okPressed())
-                                       System.exit(0);
-                               Global.cipherPassword = dialog.getPassword();
-                               goodCheck = databaseCheck(Global.getDatabaseUrl(), Global.getDatabaseUserid(), Global.getDatabaseUserPassword(), Global.cipherPassword);
-                       }
-               }
-               Global.dbRunner = new DBRunner(Global.getDatabaseUrl(), Global.getDatabaseUserid(), Global.getDatabaseUserPassword(), Global.cipherPassword);
+
                logger.log(logger.EXTREME, tr("Starting DB thread"));
                dbThread = new QThread(Global.dbRunner, "Database Thread");
                dbThread.start();
@@ -620,17 +601,29 @@ public class NeverNote extends QMainWindow{
                QPixmap pixmap = new QPixmap("classpath:cx/fbn/nevernote/icons/splash_logo.png");
                QSplashScreen splash = new QSplashScreen(pixmap);
 
-               try {
-                   initializeGlobalSettings(args);
-               } catch (InitializationException e) {
-                       QMessageBox.critical(null, "Startup error", "Aborting: " + e.getMessage());
-                       return;
-               }
+               boolean showSplash;
+               DatabaseConnection dbConn;
+
+        try {
+            initializeGlobalSettings(args);
+
+            showSplash = Global.isWindowVisible("SplashScreen");
+            if (showSplash)
+                splash.show();
 
-               boolean showSplash = Global.isWindowVisible("SplashScreen");
-               if (showSplash) 
-                       splash.show();
-               NeverNote application = new NeverNote();
+            dbConn = setupDatabaseConnection();
+
+            // Must be last stage of setup - only safe once DB is open hence we know we are the only instance running
+            Global.getFileManager().purgeResDirectory();
+
+        } catch (InitializationException e) {
+            // Fatal
+            e.printStackTrace();
+            QMessageBox.critical(null, "Startup error", "Aborting: " + e.getMessage());
+            return;
+        }
+
+        NeverNote application = new NeverNote(dbConn);
                application.setAttribute(WidgetAttribute.WA_DeleteOnClose, true);
                if (Global.wasWindowMaximized())
                        application.showMaximized();
@@ -643,6 +636,33 @@ public class NeverNote extends QMainWindow{
                QApplication.exit();
        }
 
+    /**
+     * Open the internal database, or create if not present
+     *
+     * @throws InitializationException when opening the database fails, e.g. because another process has it locked
+     */
+    private static DatabaseConnection setupDatabaseConnection() throws InitializationException {
+        DatabaseConnection dbConn = new DatabaseConnection(Global.mainThreadId);
+        dbConn.dbSetup();
+
+        Global.dbRunnerSignal = new DBRunnerSignal();
+        if (Global.getDatabaseUrl().toUpperCase().indexOf("CIPHER=") > -1) {
+            boolean goodCheck = false;
+            while (!goodCheck) {
+                DatabaseLoginDialog dialog = new DatabaseLoginDialog();
+                dialog.exec();
+                if (!dialog.okPressed())
+                    System.exit(0);
+                Global.cipherPassword = dialog.getPassword();
+                goodCheck = databaseCheck(Global.getDatabaseUrl(), Global.getDatabaseUserid(),
+                        Global.getDatabaseUserPassword(), Global.cipherPassword);
+            }
+        }
+        Global.dbRunner = new DBRunner(Global.getDatabaseUrl(), Global.getDatabaseUserid(),
+                Global.getDatabaseUserPassword(), Global.cipherPassword);
+        return dbConn;
+    }
+
        private static void initializeGlobalSettings(String[] args) throws InitializationException {
                 StartupConfig startupConfig = new StartupConfig();
 
@@ -764,7 +784,6 @@ public class NeverNote extends QMainWindow{
                        e.printStackTrace();
                }
                logger.log(logger.EXTREME, "DB Thread has terminated");
-               unlockApplication();
                logger.log(logger.HIGH, "Leaving NeverNote.closeEvent");
        }
 
@@ -832,56 +851,6 @@ public class NeverNote extends QMainWindow{
            browserWindow.resourceSignal.contentChanged.connect(this, "externalFileEdited(String)");
 //         browserWindow.resourceSignal.externalFileEdit.connect(this, "saveResourceLater(String, String)");
        }
-       private boolean lockApplication() {
-                               
-               // NFC TODO: who creates this - H2? should it be parameterized with databaseName like in RDatabaseConnection? 
-               String fileName = Global.getFileManager().getDbDirPath("NeverNote.lock.db");
-//             QFile.remove(fileName);
-               if (QFile.exists(fileName)) {
-                       QMessageBox.question(this, "Lock File Detected",
-                                       "While starting I've found a database lock file.\n" +
-                                       "to prevent multiple instances from accessing the database \n"+
-                                       "at the same time.  Please stop any other program, or (if you\n" +
-                                       "are sure nothing else is using the database) remove the file\n" +
-                                       fileName +".");
-                       return false;
-                       
-               }
-               return true;
-/*             String fileName = Global.currentDir +"nevernote.lock";
-
-               
-               if (QFile.exists(fileName)) {
-                       if (QMessageBox.question(this, "Confirmation",
-                               "While starting I've found a lock file.  This file is used to prevent multiple "+
-                               "instances of this program running at once.  If NeverNote has crashed this " +
-                               "is just a file that wasn't cleaned up and you can safely, "+
-                               "continue, but if there is another instance of this running you are " +
-                               "running the risk of creating problems.\n\n" +
-                               "Are you sure you want to continue?",
-                               QMessageBox.StandardButton.Yes, 
-                               QMessageBox.StandardButton.No)==StandardButton.No.value()) {
-                                       return false;
-                               }
-               }
-               
-               QFile file = new QFile(fileName);
-               file.open(OpenModeFlag.WriteOnly);
-               file.write(new QByteArray("This file is used to prevent multiple instances " +
-                               "of NeverNote running more than once.  " +
-                               "It should be deleted when NeverNote ends"));
-               file.close();
-               return true;
-*/
-       }
-       private void unlockApplication() {
-               // NFC TODO: should this be removed? Looks like H2 now handles the locking, which it will clean up itself. See #lockApplication.
-               String fileName = Global.getFileManager().getHomeDirPath("nevernote.lock");
-               if (QFile.exists(fileName)) {
-                       QFile.remove(fileName);
-               }
-       }
-       
 
        //***************************************************************
        //***************************************************************
@@ -4844,13 +4813,10 @@ public class NeverNote extends QMainWindow{
                }
        }
 
-
-       
-       
        //*************************************************
        //* Check database userid & passwords
        //*************************************************
-       public boolean databaseCheck(String url,String userid, String userPassword, String cypherPassword) {
+       private static boolean databaseCheck(String url,String userid, String userPassword, String cypherPassword) {
                        Connection connection;
                        
                        try {
index d27764a..db91568 100644 (file)
@@ -33,9 +33,10 @@ public class FileManager {
     private final File xmlDir;
 
     /**
-     * Check or create the db, log and res directories, and purge files from 'res' .
+     * Check or create the db, log and res directories.
      *
      * @param homeDirPath the installation dir containing db/log/res directories, must exist
+     * @throws InitializationException for missing directories or file permissions problems
      */
     public FileManager(String homeDirPath) throws InitializationException {
         if (homeDirPath == null) {
@@ -69,8 +70,6 @@ public class FileManager {
         resDir = new File(homeDir, "res");
         createDirOrCheckWriteable(resDir);
         resDirPath = slashTerminatePath(resDir.getPath());
-
-        deleteTopLevelFiles(resDir);
     }
 
     /**
@@ -232,4 +231,11 @@ public class FileManager {
             throw new InitializationException("Directory '" + dir + "' does not have write permission");
         }
     }
+
+    /**
+     * Called at startup to purge files from 'res' directory.
+     */
+    public void purgeResDirectory() throws InitializationException {
+        deleteTopLevelFiles(resDir);
+    }
 }
index f41ad3a..a8ed41b 100644 (file)
@@ -12,4 +12,8 @@ public class InitializationException extends Exception {
         super(message);
     }
 
+    public InitializationException(String message, Throwable cause) {
+        super(message, cause);
+    }
+
 }
index d26126a..b754828 100644 (file)
@@ -22,7 +22,6 @@ import java.io.File;
 
 import cx.fbn.nevernote.Global;
 import cx.fbn.nevernote.sql.requests.DatabaseRequest;
-import cx.fbn.nevernote.utilities.ApplicationLogger;
 
 
 public class DatabaseConnection {
@@ -36,11 +35,10 @@ public class DatabaseConnection {
        private final WatchFolderTable                  watchFolderTable;
        private final InvalidXMLTable                   invalidXMLTable;
        private final SyncTable                                 syncTable;
-       private final ApplicationLogger                 logger;
        int id;
 
        
-       public DatabaseConnection(ApplicationLogger l, int i) {
+       public DatabaseConnection(int i) {
                id = i;
                tagTable = new TagTable(id);
                notebookTable = new NotebookTable(id);
@@ -51,15 +49,11 @@ public class DatabaseConnection {
                wordsTable = new WordsTable(id);
                invalidXMLTable = new InvalidXMLTable(id);
                syncTable = new SyncTable(id);
-               logger = l;
-
        }
        
        
        // Initialize the database connection
        public void dbSetup() {
-               logger.log(logger.HIGH, "Entering DatabaseConnection.dbSetup " +id);
-
                // NFC FIXME: should be parameterized with databaseName like in RDatabaseConnection?
                File f = Global.getFileManager().getDbDirFile("NeverNote.h2.db");
                boolean dbExists = f.exists(); 
@@ -69,8 +63,6 @@ public class DatabaseConnection {
                        createTables();
                        Global.setAutomaticLogin(false);
                }
-               
-               logger.log(logger.HIGH, "Leaving DatabaseConnection.dbSetup" +id);
        }
        
        
@@ -113,6 +105,7 @@ public class DatabaseConnection {
        }
        
        public void checkDatabaseVersion() {
+               // NFC FIXME: this needs to read the existing version number from a table in the DB
                if (!Global.getDatabaseVersion().equals("0.86")) {
                        upgradeDb(Global.getDatabaseVersion());
                }
@@ -137,7 +130,7 @@ public class DatabaseConnection {
        }
        
        
-       public void createTables() {
+       private void createTables() {
                Global.setDatabaseVersion("0.85");
 //             Global.setUpdateSequenceNumber(0);
                Global.setAutomaticLogin(false);
index 940a833..826de36 100644 (file)
@@ -21,9 +21,9 @@ package cx.fbn.nevernote.sql.requests;
 \r
 \r
 public class DatabaseRequest extends DBRunnerRequest {\r
+        // NFC TODO: change to use an Enum and clarify distinction with constants on DBRunnerRequest \r
        public static int Create_Tables                         = 1;\r
        public static int Drop_Tables                           = 2;\r
-       public static int Setup                                         = 3;\r
        public static int Shutdown                                      = 4;\r
        public static int Compact                                       = 5;\r
        public static int Execute_Sql               = 6;\r
index 155a151..3a36a81 100644 (file)
@@ -26,6 +26,7 @@ import java.sql.SQLException;
 import com.trolltech.qt.sql.QJdbc;
 
 import cx.fbn.nevernote.Global;
+import cx.fbn.nevernote.config.InitializationException;
 import cx.fbn.nevernote.sql.driver.NSqlQuery;
 import cx.fbn.nevernote.utilities.ApplicationLogger;
 
@@ -45,7 +46,7 @@ public class RDatabaseConnection {
        private RSyncTable                                      syncTable;
 
        
-       public RDatabaseConnection(ApplicationLogger l, String c) {
+       public RDatabaseConnection(ApplicationLogger l) {
                logger = l;
                databaseName = Global.databaseName;
        }
@@ -68,7 +69,11 @@ public class RDatabaseConnection {
     //***************************************************************
     //***************************************************************
        // Initialize the database connection
-       public void dbSetup(String url,String userid, String userPassword, String cypherPassword) {
+       /**
+        * @throws InitializationException for missing driver, bad connection URL / user / password, or DB locked by another process
+        */
+       public void dbSetup(String url,String userid, String userPassword, String cypherPassword)
+               throws InitializationException {
                logger.log(logger.HIGH, "Entering RDatabaseConnection.dbSetup");
                
                // This thread cleans things up if we crash
@@ -84,13 +89,14 @@ public class RDatabaseConnection {
                        }
 */
 
+               final String driverClassName = "org.h2.Driver";
                try {
-                       Class.forName("org.h2.Driver");
+                        Class.forName(driverClassName);
                } catch (ClassNotFoundException e1) {
-                       e1.printStackTrace();
-                       System.exit(16);
+                       throw new InitializationException("Cannot find JDBC driver class '" + driverClassName
+                               + "', check jar is in the classpath");
                }
-               
+
                QJdbc.initialize();
 //             db = QSqlDatabase.addDatabase("QSQLITE", connectionName);               
 //             db = QSqlDatabase.addDatabase("QJDBC", connectionName); 
@@ -109,10 +115,24 @@ public class RDatabaseConnection {
                                passwordString = cypherPassword+" "+userPassword;
                        conn = DriverManager.getConnection(url,userid,passwordString);
                } catch (SQLException e) {
-                       e.printStackTrace();
-                       return;
+                       File lockFile = Global.getFileManager().getDbDirFile(databaseName + ".lock.db");
+                       if (lockFile.exists()) {
+                           throw new InitializationException("H2 database is locked,\n"
+                                       + "probably because another NeverNote instance is already using it.\n"
+                                       + "Please close the other instance, or run them from separate directories.",
+                                       e);
+                       } else if (dbExists) {
+                            throw new InitializationException("Cannot open existing H2 database,\n"
+                                        + "maybe it is encrypted or corrupt?",
+                                        e);
+                       } else {
+                            throw new InitializationException("Cannot create H2 database,\n"
+                                        + "check URL and filesystem permissions",
+                                        e);
+                       }
                }
 
+               // NFC TODO: change the low-level commands to propagate exceptions so we can detect them here
                if (!dbExists)
                        createTables();
                
index 188ceca..6ad627d 100644 (file)
@@ -166,7 +166,7 @@ public class CounterRunner extends QObject implements Runnable {
                logger.log(logger.EXTREME, "Entering ListManager.countNotebookResults");                \r
                if (abortCount)\r
                        return;\r
-               DatabaseConnection conn = new DatabaseConnection(logger, Global.tagCounterThreadId);\r
+               DatabaseConnection conn = new DatabaseConnection(Global.tagCounterThreadId);\r
                List<NotebookCounter> nCounter = new ArrayList<NotebookCounter>();\r
                if (abortCount)\r
                        return;\r
@@ -245,7 +245,7 @@ public class CounterRunner extends QObject implements Runnable {
        \r
        private void countTagResults() {\r
                logger.log(logger.EXTREME, "Entering ListManager.countTagResults");             \r
-               DatabaseConnection conn = new DatabaseConnection(logger, Global.tagCounterThreadId);\r
+               DatabaseConnection conn = new DatabaseConnection(Global.tagCounterThreadId);\r
                List<TagCounter> counter = new ArrayList<TagCounter>();\r
                List<Tag> allTags = conn.getTagTable().getAll();\r
                \r
@@ -315,7 +315,7 @@ public class CounterRunner extends QObject implements Runnable {
        \r
        private void countTrashResults() {\r
                logger.log(logger.EXTREME, "Entering CounterRunner.countTrashResults()");               \r
-               DatabaseConnection conn = new DatabaseConnection(logger, Global.trashCounterThreadId);\r
+               DatabaseConnection conn = new DatabaseConnection(Global.trashCounterThreadId);\r
                if (abortCount)\r
                        return;\r
 \r
index 75c76bd..58b3df1 100644 (file)
@@ -25,6 +25,7 @@ import java.util.concurrent.LinkedBlockingQueue;
 import com.trolltech.qt.core.QObject;\r
 \r
 import cx.fbn.nevernote.Global;\r
+import cx.fbn.nevernote.config.InitializationException;\r
 import cx.fbn.nevernote.signals.DBRunnerSignal;\r
 import cx.fbn.nevernote.sql.requests.DBRunnerRequest;\r
 import cx.fbn.nevernote.sql.requests.DatabaseRequest;\r
@@ -45,11 +46,13 @@ import cx.fbn.nevernote.sql.runners.REnSearch;
 import cx.fbn.nevernote.utilities.ApplicationLogger;\r
 \r
 public class DBRunner extends QObject implements Runnable {\r
-       private ApplicationLogger                       logger;\r
+       private final ApplicationLogger                         logger;\r
        \r
-       private RDatabaseConnection             conn;\r
+       private final RDatabaseConnection               conn;\r
+\r
+       // NFC TODO: why do these need to be volatile?\r
        private volatile LinkedBlockingQueue<DBRunnerRequest> workQueue;\r
-       \r
+\r
        public volatile Vector<DBRunnerRequest>         genericResponse;\r
        public volatile Vector<DeletedItemRequest>      deletedItemResponse;\r
        public volatile Vector<NotebookRequest>         notebookResponse;\r
@@ -79,14 +82,17 @@ public class DBRunner extends QObject implements Runnable {
        private final String userid;\r
        private final String userPassword;\r
        \r
-       public DBRunner(String u, String id, String pass, String cypher) {\r
+       /**\r
+        * @throws InitializationException when opening the database fails\r
+        */\r
+       public DBRunner(String u, String id, String pass, String cypher) throws InitializationException {\r
                workQueue=new LinkedBlockingQueue<DBRunnerRequest>(MAX_QUEUED_WAITING);\r
 \r
                url=u;\r
                userid = id;\r
                userPassword = pass;\r
                cypherPassword=cypher;\r
-               \r
+\r
                //***********************************************\r
                //* These are the priority queues.    \r
                //***********************************************\r
@@ -153,18 +159,16 @@ public class DBRunner extends QObject implements Runnable {
                for (int i=0; i<Global.dbThreadId; i++)\r
                        syncResponse.add(new SyncRequest());\r
 \r
-\r
-               \r
                dbSignal = new DBRunnerSignal();\r
-               \r
+\r
+                logger = new ApplicationLogger("dbrunner.log");\r
+\r
+                conn = new RDatabaseConnection(logger);\r
+                conn.dbSetup(url, userid, userPassword, cypherPassword);\r
        }\r
 \r
        \r
        public void run() {\r
-               logger = new ApplicationLogger("dbrunner.log");\r
-               conn = new RDatabaseConnection(logger, "dbrunner");\r
-               conn.dbSetup(url,userid, userPassword, cypherPassword);\r
-       \r
                thread().setPriority(Thread.NORM_PRIORITY);\r
                \r
                \r
@@ -413,9 +417,6 @@ public class DBRunner extends QObject implements Runnable {
                        conn.compactDatabase();\r
                        release(r.requestor_id);\r
                        return;\r
-               } else if (r.type == DatabaseRequest.Setup) {\r
-                       conn.dbSetup(url,userid, userPassword, cypherPassword);\r
-                       return;\r
                } else if (r.type == DatabaseRequest.Shutdown) {\r
                        conn.dbShutdown();\r
                        keepRunning = false;\r
index 129126e..80d4615 100644 (file)
@@ -62,7 +62,7 @@ public class IndexRunner extends QObject implements Runnable {
        public IndexRunner(String logname) {\r
                logger = new ApplicationLogger(logname);\r
                threadID = Global.indexThreadId;\r
-               conn = new DatabaseConnection(logger, threadID);\r
+               conn = new DatabaseConnection(threadID);\r
                noteSignal = new NoteSignal();\r
                resourceSignal = new NoteResourceSignal();\r
 //             threadSignal = new ThreadSignal();\r
index 7fdd135..510055f 100644 (file)
@@ -51,7 +51,7 @@ public class SaveRunner extends QObject implements Runnable {
        public SaveRunner(String logname) {\r
                logger = new ApplicationLogger(logname);\r
                threadID = Global.saveThreadId;\r
-               conn = new DatabaseConnection(logger, threadID);\r
+               conn = new DatabaseConnection(threadID);\r
                threadLock = new QMutex();\r
                keepRunning = true;\r
        }\r
index 5ce4636..45b73ef 100644 (file)
@@ -130,7 +130,7 @@ public class SyncRunner extends QObject implements Runnable {
                resourceSignal = new NoteResourceSignal();\r
                \r
 //             this.setAutoDelete(false);\r
-               conn = new DatabaseConnection(logger, Global.syncThreadId);\r
+               conn = new DatabaseConnection(Global.syncThreadId);\r
                isConnected = false;\r
                syncNeeded = false;\r
                authRefreshNeeded = false;\r