org.pgptool.gui.tools.singleinstance.SingleInstanceFileBasedImpl.java Source code

Java tutorial

Introduction

Here is the source code for org.pgptool.gui.tools.singleinstance.SingleInstanceFileBasedImpl.java

Source

/*******************************************************************************
 * PGPTool is a desktop application for pgp encryption/decryption
 * Copyright (C) 2017 Sergey Karpushin
 *
 * 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 3 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 org.pgptool.gui.tools.singleinstance;

import java.io.File;
import java.lang.management.ManagementFactory;
import java.nio.file.Path;
import java.nio.file.WatchEvent;

import org.apache.commons.io.FilenameUtils;
import org.apache.log4j.Logger;
import org.pgptool.gui.config.impl.ConfigRepositoryImpl;
import org.pgptool.gui.tools.IoStreamUtils;
import org.pgptool.gui.tools.dirwatcher.DirWatcherHandler;
import org.pgptool.gui.tools.dirwatcher.SingleDirWatcher;
import org.pgptool.gui.ui.tools.FileBasedLock;

import com.google.common.base.Preconditions;

/**
 * This impl will create temp dir and will monitor files in this dir. Each
 * secondary instance will create file there with arguments. Primary instance
 * will watch this dear, read these files and pass arguments for processing to
 * {@link PrimaryInstanceListener}. Primary instance will hold exclusive lock on
 * a file inside this temp folder
 * 
 * @author Sergey Karpushin
 *
 */
public class SingleInstanceFileBasedImpl implements SingleInstance {
    private static Logger log = Logger.getLogger(SingleInstanceFileBasedImpl.class);

    private static final int LOCK_ARGS_SUBMISSION_TIMEOUT = 3000;
    private static final String ROLE_LOCK_FILE_EXTENSION = ".role-lock";
    private static final String DIR_LOCK_FILE_EXTENSION = ".dir-lock";
    private static final String PARAMS_FILE_EXTENSION = "args";
    private static final String PARAMS_FILE_EXTENSION_TEMP = "temp";

    private String tagName;
    private PrimaryInstanceListener primaryInstanceListener;

    private FileBasedLock lockRole;

    /**
     * This additional lock will be used by parties to enter critical section before
     * writing any changes to this directory. It will help us avoid any race
     * conditions
     */
    private FileBasedLock lockNewArgsSubmissons;
    private SingleDirWatcher singleDirWatcher;
    private String basePathForCommands;

    /**
     * @param tagName
     *            must be a valid folder name
     */
    public SingleInstanceFileBasedImpl(String tagName) {
        this.tagName = tagName;
        String baseTempPath = System.getProperty("java.io.tmpdir");
        basePathForCommands = getDirForSingleInstance(baseTempPath);

        try {
            lockNewArgsSubmissons = new FileBasedLock(
                    basePathForCommands + File.separator + tagName + DIR_LOCK_FILE_EXTENSION);
        } catch (Throwable t) {
            throw new RuntimeException("Failed to init lock file object", t);
        }
    }

    @Override
    public boolean tryClaimPrimaryInstanceRole(PrimaryInstanceListener primaryInstanceListener) {
        try {
            lockNewArgsSubmissons.tryLockWaitMs(LOCK_ARGS_SUBMISSION_TIMEOUT);

            lockRole = new FileBasedLock(basePathForCommands + File.separator + tagName + ROLE_LOCK_FILE_EXTENSION);
            if (!lockRole.tryLock()) {
                log.info("This instance is not condiered as a primary instance");
                return false;
            }
            log.info("From now on this instance considered as a primary instance");

            Runtime.getRuntime().addShutdownHook(shutDownHook);

            this.primaryInstanceListener = primaryInstanceListener;
            singleDirWatcher = new SingleDirWatcher(basePathForCommands, dirWatcherHandler);
            return true;
        } catch (Throwable t) {
            // we need to release lock because apparently we cannot watch for
            // changes
            IoStreamUtils.safeClose(lockRole);
            throw new RuntimeException("Failed to setup file watcher", t);
        } finally {
            lockNewArgsSubmissons.releaseLock();
        }
    }

    private String getDirForSingleInstance(String baseTempPath) {
        File singleInstFolder = new File(baseTempPath + File.separator + tagName);
        String ret = singleInstFolder.getAbsolutePath();
        Preconditions.checkState(singleInstFolder.exists() || singleInstFolder.mkdirs(),
                "Cannot ensure sync folder for multiple instances: " + ret);
        return ret;
    }

    private DirWatcherHandler dirWatcherHandler = new DirWatcherHandler() {
        @Override
        public void handleEvent(WatchEvent<?> event, Path node) {
            String fileName = node.toString();
            if (!PARAMS_FILE_EXTENSION.equalsIgnoreCase(FilenameUtils.getExtension(fileName))) {
                return;
            }

            try {
                InvokePrimaryInstanceArgs args = tryReadArgs(fileName);
                Preconditions.checkState(args != null, "Failed to read args file");
                primaryInstanceListener.handleArgsFromOtherInstance(args.getCommandLineArgs());
                safeDelete(fileName);
            } catch (Throwable t) {
                log.error("Failed to handle single instance command", t);
            }
        }

        private void safeDelete(String fileName) {
            try {
                new File(fileName).delete();
            } catch (Throwable t) {
                log.warn("Failed to remove commands file", t);
            }
        }

        /**
         * We might need to perform couple attempts because rename action initiated by
         * other instance might block args file
         */
        private InvokePrimaryInstanceArgs tryReadArgs(String fileName) throws InterruptedException {
            long timeoutAt = System.currentTimeMillis() + LOCK_ARGS_SUBMISSION_TIMEOUT;
            InvokePrimaryInstanceArgs args = ConfigRepositoryImpl.readObject(fileName);
            while (args == null && System.currentTimeMillis() < timeoutAt) {
                Thread.sleep(50);
                args = ConfigRepositoryImpl.readObject(fileName);
            }
            return args;
        }

        @Override
        public void watcherHasToStop() {
            // Not sure -- do we need to handle it somehow? If lock on file was
            // acquired this should never happen (this method should never be
            // called)
        }
    };

    private Thread shutDownHook = new Thread() {
        @Override
        public void run() {
            IoStreamUtils.safeClose(lockRole);
            if (singleDirWatcher != null) {
                singleDirWatcher.stopWatcher();
                singleDirWatcher = null;
            }
        }
    };

    @Override
    public boolean sendArgumentsToOtherInstance(String[] args) {
        if (primaryInstanceListener != null) {
            // how come?!!
            primaryInstanceListener.handleArgsFromOtherInstance(args);
            return true;
        }

        try {
            if (!lockNewArgsSubmissons.tryLockWaitMs(LOCK_ARGS_SUBMISSION_TIMEOUT)) {
                return false;
            }
            File targetFile = sendCommand(args);
            return isCommandReceived(targetFile);
        } catch (Throwable t) {
            throw new RuntimeException("Failed to submit args", t);
        } finally {
            lockNewArgsSubmissons.releaseLock();
        }
    }

    private boolean isCommandReceived(File targetFile) throws InterruptedException {
        long timeoutAt = System.currentTimeMillis() + LOCK_ARGS_SUBMISSION_TIMEOUT;
        while (targetFile.exists()) {
            Thread.sleep(50);
            if (System.currentTimeMillis() >= timeoutAt) {
                log.warn("As a secondary instance we can't see confirmation that our args were received "
                        + targetFile);
                return false;
            }
        }
        log.info("As a secondary we see args were processed by primary instance " + targetFile);
        return true;
    }

    private File sendCommand(String[] args) {
        String fileName = basePathForCommands + File.separator + getProcessId() + "_" + System.currentTimeMillis();
        String tempFileName = fileName + "." + PARAMS_FILE_EXTENSION_TEMP;
        log.debug("Creating temp args file: " + tempFileName);
        ConfigRepositoryImpl.writeObject(new InvokePrimaryInstanceArgs(args), tempFileName);
        log.debug("Done creating temp args file: " + tempFileName);
        File targetFile = new File(fileName + "." + PARAMS_FILE_EXTENSION);
        log.debug("Renaming temp args file: " + tempFileName);
        new File(tempFileName).renameTo(targetFile);
        log.debug("Done renaming temp args file: " + tempFileName);
        return targetFile;
    }

    private static String getProcessId() {
        String jvmName = ManagementFactory.getRuntimeMXBean().getName();
        return jvmName.split("@")[0];
    }

    @Override
    public boolean isPrimaryInstance() {
        return primaryInstanceListener != null;
    }

}