package br.uff.ic.dyevc.monitor; //~--- non-JDK imports -------------------------------------------------------- import br.uff.ic.dyevc.beans.ApplicationSettingsBean; import br.uff.ic.dyevc.exception.DyeVCException; import br.uff.ic.dyevc.exception.VCSException; import br.uff.ic.dyevc.gui.core.MessageManager; import br.uff.ic.dyevc.gui.main.MainWindow; import br.uff.ic.dyevc.model.BranchStatus; import br.uff.ic.dyevc.model.MonitoredRepositories; import br.uff.ic.dyevc.model.MonitoredRepository; import br.uff.ic.dyevc.model.RepositoryStatus; import br.uff.ic.dyevc.tools.vcs.git.GitConnector; import br.uff.ic.dyevc.tools.vcs.git.GitTools; import br.uff.ic.dyevc.utils.ApplicationVersionUtils; import br.uff.ic.dyevc.utils.PreferencesManager; import br.uff.ic.dyevc.utils.StopWatchLogger; import org.apache.commons.io.filefilter.FileFilterUtils; import org.apache.commons.io.FileUtils; import org.apache.commons.lang.time.StopWatch; import org.slf4j.LoggerFactory; //~--- JDK imports ------------------------------------------------------------ import java.io.File; import java.io.IOException; import java.util.ArrayList; import java.util.Collections; import java.util.List; /** * This class continuously monitors the specified repositories, according to the specified refresh rate. * * @author Cristiano */ public class RepositoryMonitor extends Thread { private final ApplicationSettingsBean settings; private List<RepositoryStatus> statusList; private MainWindow container; private final List<MonitoredRepository> monitorQueue = Collections.synchronizedList(new ArrayList<MonitoredRepository>()); private final List<MonitoredRepository> cleanAndMonitorQueue = Collections.synchronizedList(new ArrayList<MonitoredRepository>()); private MonitoredRepository repositoryToMonitor; private final String currentApplicationVersion; private final TopologyUpdater updater; private static RepositoryMonitor instance; private boolean versionChanged; private String previousApplicationVersion; /** * Associates the specified window container and continuously monitors the specified list of repositories. * */ private RepositoryMonitor() { LoggerFactory.getLogger(RepositoryMonitor.class).trace("Constructor -> Entry."); settings = PreferencesManager.getInstance().loadPreferences(); currentApplicationVersion = ApplicationVersionUtils.getInstance().getAppVersion(); this.updater = new TopologyUpdater(); LoggerFactory.getLogger(RepositoryMonitor.class).trace("Constructor -> Exit."); } /** * Provides the singleton instance * * @return the singleton instance. */ public synchronized static RepositoryMonitor getInstance() { if (instance == null) { instance = new RepositoryMonitor(); } return instance; } public void setContainer(MainWindow container) { this.container = container; } /** * Runs the monitor. After running, monitor sleeps for the time specified by the refresh interval application * property. */ @Override public void run() { LoggerFactory.getLogger(RepositoryMonitor.class).trace("Repository monitor is running."); int sleepTime = settings.getRefreshInterval() * 1000; previousApplicationVersion = settings.getLastApplicationVersionUsed(); if (!(currentApplicationVersion.equals(previousApplicationVersion))) { versionChanged = true; settings.setLastApplicationVersionUsed(currentApplicationVersion); PreferencesManager.getInstance().storePreferences(settings); } try { checkWorkingFolder(); } catch (DyeVCException ex) { return; } StopWatchLogger watch = new StopWatchLogger(RepositoryMonitor.class); while (true) { try { MessageManager.getInstance().addMessage("Repository monitor is running."); if (!monitorQueue.isEmpty() ||!cleanAndMonitorQueue.isEmpty()) { // Process pending repositories recently added to configuration while (true) { boolean discardTopologyCache = false; if (!monitorQueue.isEmpty()) { repositoryToMonitor = monitorQueue.remove(0); } else if (!cleanAndMonitorQueue.isEmpty()) { repositoryToMonitor = cleanAndMonitorQueue.remove(0); discardTopologyCache = true; } else { break; } MessageManager.getInstance().addMessage("Monitoring new repository <" + repositoryToMonitor.getName() + "> with id <" + repositoryToMonitor.getId() + ">. Check console for details."); watch.start(); checkRepository(repositoryToMonitor); watch.stopAndLog("Repository <" + repositoryToMonitor.getName() + "> with id <" + repositoryToMonitor.getId() + "> was checked."); if (!repositoryToMonitor.getRepStatus().isInvalid()) { watch.start(); updater.update(repositoryToMonitor, discardTopologyCache, versionChanged, previousApplicationVersion); watch.stopAndLog("Topology for repository <" + repositoryToMonitor.getName() + "> with id <" + repositoryToMonitor.getId() + "> was updated."); } repositoryToMonitor = null; } } else { // Normal processing LoggerFactory.getLogger(RepositoryMonitor.class).info("Found {} repositories to monitor.", MonitoredRepositories.getMonitoredProjects().size()); statusList = new ArrayList<RepositoryStatus>(); int count = 0; for (MonitoredRepository monitoredRepository : MonitoredRepositories.getMonitoredProjects()) { MessageManager.getInstance().addMessage("Checking repository " + ++count + " of " + MonitoredRepositories.getMonitoredProjects().size() + ": <" + monitoredRepository.getName() + "> with id <" + monitoredRepository.getId() + ">. Check console for details."); watch.start(); checkRepository(monitoredRepository); watch.stopAndLog("Repository <" + monitoredRepository.getName() + "> with id <" + monitoredRepository.getId() + "> was checked."); if (!monitoredRepository.getRepStatus().isInvalid()) { watch.start(); updater.update(monitoredRepository, false, versionChanged, previousApplicationVersion); watch.stopAndLog("Topology for repository <" + monitoredRepository.getName() + "> with id <" + monitoredRepository.getId() + "> was updated."); } } MessageManager.getInstance().addMessage( "Finished checking monitored repositories. Now verifying deleted repositories."); updater.verifyDeletedRepositories(); MessageManager.getInstance().addMessage( "Finished checking deleted repositories. Now persisting new information."); PreferencesManager.getInstance().persistRepositories(); } container.notifyMessages(statusList); LoggerFactory.getLogger(RepositoryMonitor.class).debug("Will now sleep for {} seconds.", sleepTime); MessageManager.getInstance().addMessage("Repository monitor is sleeping."); Thread.sleep(settings.getRefreshInterval() * 1000); LoggerFactory.getLogger(RepositoryMonitor.class).debug("Waking up after sleeping for {} seconds.", sleepTime); } catch (DyeVCException dex) { try { MessageManager.getInstance().addMessage(dex.getMessage()); Thread.sleep(settings.getRefreshInterval() * 1000); LoggerFactory.getLogger(RepositoryMonitor.class).debug("Waking up after sleeping for {} seconds.", sleepTime); } catch (InterruptedException ex) { LoggerFactory.getLogger(RepositoryMonitor.class).info("Waking up due to interruption received."); } } catch (RuntimeException re) { try { MessageManager.getInstance().addMessage(re.getMessage()); LoggerFactory.getLogger(RepositoryMonitor.class).error("Error during monitoring.", re); Thread.sleep(settings.getRefreshInterval() * 1000); LoggerFactory.getLogger(RepositoryMonitor.class).debug("Waking up after sleeping for {} seconds.", sleepTime); } catch (InterruptedException ex) { LoggerFactory.getLogger(RepositoryMonitor.class).info("Waking up due to interruption received."); } } catch (InterruptedException ex) { LoggerFactory.getLogger(RepositoryMonitor.class).info("Waking up due to interruption received."); } } } /** * Checks if a given repository is behind and/or ahead its tracking remotes, populating a status list according to * the results. * * @param monitoredRepository the repository to be checked */ private void checkRepository(MonitoredRepository monitoredRepository) { LoggerFactory.getLogger(RepositoryMonitor.class).trace("checkRepository -> Entry. Repository: {}, id:{}", monitoredRepository.getName(), monitoredRepository.getId()); RepositoryStatus repStatus = new RepositoryStatus(monitoredRepository.getId()); String cloneAddress = monitoredRepository.getCloneAddress(); if (!GitConnector.isValidRepository(cloneAddress)) { repStatus.setInvalid("<" + cloneAddress + "> is not a valid repository path."); } else { try { List<BranchStatus> status = processRepository(monitoredRepository); repStatus.addBranchStatusList(status); } catch (VCSException ex) { MessageManager.getInstance().addMessage("It was not possible to finish monitoring of repository <" + monitoredRepository.getName() + "> with id <" + monitoredRepository.getId() + ">."); LoggerFactory.getLogger(RepositoryMonitor.class).error( "It was not possible to finish monitoring of repository <{}> with id <{}>", monitoredRepository.getName(), monitoredRepository.getId()); repStatus.setInvalid(ex.getCause().toString()); } } monitoredRepository.setRepStatus(repStatus); statusList.add(repStatus); LoggerFactory.getLogger(RepositoryMonitor.class).trace("checkRepository -> Exit. Repository: {}", monitoredRepository.getName()); } /** * Process a valid repository to check its behind and ahead status * * @param monitoredRepository the repository to be processed * @return a list of status for the given repository */ private List<BranchStatus> processRepository(MonitoredRepository monitoredRepository) throws VCSException { LoggerFactory.getLogger(RepositoryMonitor.class).trace("processRepository -> Entry. Repository: {}", monitoredRepository.getName()); GitConnector sourceConnector = null; GitConnector tempConnector = null; List<BranchStatus> result = null; try { sourceConnector = monitoredRepository.getConnection(); LoggerFactory.getLogger(RepositoryMonitor.class).debug( "processRepository -> created gitConnector for repository {}, id={}", monitoredRepository.getName(), monitoredRepository.getId()); String pathTemp = monitoredRepository.getWorkingCloneAddress(); if (!GitConnector.isValidRepository(pathTemp)) { LoggerFactory.getLogger(RepositoryMonitor.class).debug( "There is no valid temp repository at {}. Will create a valid temp by cloning {}.", pathTemp, monitoredRepository.getId()); monitoredRepository.setWorkingCloneConnection(createWorkingClone(pathTemp, sourceConnector)); } tempConnector = monitoredRepository.getWorkingCloneConnection(); GitTools.adjustTargetConfiguration(sourceConnector, tempConnector); tempConnector.fetchAllRemotes(true); result = tempConnector.testAhead(); } finally { if (sourceConnector != null) { LoggerFactory.getLogger(RepositoryMonitor.class).debug( "About to close connection with repository <{}> with id <{}>", monitoredRepository.getName(), monitoredRepository.getId()); sourceConnector.close(); } if (tempConnector != null) { LoggerFactory.getLogger(RepositoryMonitor.class).debug( "About to close connection with temp repository for <{}> with id <{}>", monitoredRepository.getName(), monitoredRepository.getId()); tempConnector.close(); } } LoggerFactory.getLogger(RepositoryMonitor.class).trace("processRepository -> Exit. Repository: {}", monitoredRepository.getName()); return result; } /** * Verifies whether the working folder exists or not. If false, creates it, otherwise, looks for orphaned folders * related to projects not monitored anymore. * * @throws DyeVCException */ private void checkWorkingFolder() throws DyeVCException { LoggerFactory.getLogger(RepositoryMonitor.class).trace("checkWorkingFolder -> Entry."); File workingFolder = new File(settings.getWorkingPath()); LoggerFactory.getLogger(RepositoryMonitor.class).info("checkWorkingFolder -> Working folder is at {}.", workingFolder.getAbsoluteFile()); if (workingFolder.exists()) { LoggerFactory.getLogger(RepositoryMonitor.class).debug("Working folder already exists."); checkOrphanedFolders(workingFolder); if (!workingFolder.canWrite()) { LoggerFactory.getLogger(RepositoryMonitor.class).error("Working folder is not writable."); MessageManager.getInstance().addMessage( "Working folder is not writable. Please check folder permissions at " + workingFolder.getAbsolutePath()); throw new DyeVCException("Temp folder located at " + workingFolder.getAbsolutePath() + " is not writable. Cannot run monitor."); } } else { workingFolder.mkdir(); LoggerFactory.getLogger(RepositoryMonitor.class).debug( "Working folder does not exist. A brand new one was created."); } LoggerFactory.getLogger(RepositoryMonitor.class).trace("checkWorkingFolder -> Exit."); } /** * Deletes folders with clones related to projects that are not monitored anymore. * * @param pointer to the working folder */ private void checkOrphanedFolders(File workingFolder) { LoggerFactory.getLogger(RepositoryMonitor.class).trace("checkOrphanedFolders -> Entry."); String[] tmpFolders = workingFolder.list(FileFilterUtils.directoryFileFilter()); for (String tmpFolder : tmpFolders) { if (MonitoredRepositories.getMonitoredProjectById(tmpFolder) == null) { LoggerFactory.getLogger(RepositoryMonitor.class).debug( "Repository with id={} is not being monitored anymore. Temp folder will be deleted.", tmpFolder); deleteDirectory(workingFolder, tmpFolder); } } LoggerFactory.getLogger(RepositoryMonitor.class).trace("checkOrphanedFolders -> Exit."); } /** * Creates a working clone for the repository and copies the source configuration to the clone * * @param pathTemp the path where the clone will be created * @param source the source to be cloned * @return a GitConnector pointing to temp clone * @throws VCSException */ private GitConnector createWorkingClone(String pathTemp, GitConnector source) throws VCSException { LoggerFactory.getLogger(RepositoryMonitor.class).trace("createWorkingClone -> Entry."); GitConnector target = null; try { if (new File(pathTemp).exists()) { FileUtils.cleanDirectory(new File(pathTemp)); LoggerFactory.getLogger(RepositoryMonitor.class).debug("Removed existing content at {}. ", pathTemp); } target = source.cloneThis(pathTemp); GitTools.adjustTargetConfiguration(source, target); LoggerFactory.getLogger(RepositoryMonitor.class).debug("Created temp clone."); } catch (IOException ex) { LoggerFactory.getLogger(RepositoryMonitor.class).error("Error cleaning existing temp folder at" + pathTemp + ".", ex); throw new VCSException("Error cleaning existing temp folder at" + pathTemp + ".", ex); } LoggerFactory.getLogger(RepositoryMonitor.class).trace("createWorkingClone -> Entry."); return target; } /** * Removes pathToDelete from the specified parent folder. * * @param parentFolder Folder within path to delete resides. * @param pathToDelete path to be deleted. Can be the name of a file or folder */ private void deleteDirectory(File parentFolder, String pathToDelete) { try { FileUtils.deleteDirectory(new File(parentFolder, pathToDelete)); LoggerFactory.getLogger(RepositoryMonitor.class).debug("Folder {} was successfully deleted.", pathToDelete); } catch (IOException ex) { LoggerFactory.getLogger(RepositoryMonitor.class).error("It was not possible to delete folder " + pathToDelete, ex); } } /** * Adds a repository to the queue to be monitored as soon as the current run finishes * * @param repos the repositoryToMonitor to add */ public synchronized void addRepositoryToMonitor(MonitoredRepository repos) { monitorQueue.add(repos); } /** * Adds a repository to the forcedQueue to be monitored as soon as the current run finishes, discarding its cache * * @param repos the repositoryToMonitor to add */ public synchronized void addRepositoryToCleanAndMonitor(MonitoredRepository repos) { cleanAndMonitorQueue.add(repos); } }