Java tutorial
// Copyright (c) Microsoft. All rights reserved. // Licensed under the MIT license. See License.txt in the repository root. package com.microsoft.tfs.client.common.server.cache.buildstatus; import java.text.MessageFormat; import java.util.ArrayList; import java.util.HashMap; import java.util.Iterator; import java.util.List; import java.util.Map; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.eclipse.core.runtime.CoreException; import org.eclipse.core.runtime.IConfigurationElement; import org.eclipse.core.runtime.IExtensionPoint; import org.eclipse.core.runtime.IExtensionRegistry; import org.eclipse.core.runtime.IProgressMonitor; import org.eclipse.core.runtime.IStatus; import org.eclipse.core.runtime.Platform; import org.eclipse.core.runtime.Preferences; import org.eclipse.core.runtime.Status; import org.eclipse.core.runtime.jobs.Job; import com.microsoft.tfs.client.common.Messages; import com.microsoft.tfs.client.common.TFSCommonClientPlugin; import com.microsoft.tfs.client.common.codemarker.CodeMarker; import com.microsoft.tfs.client.common.codemarker.CodeMarkerDispatch; import com.microsoft.tfs.client.common.prefs.PreferenceConstants; import com.microsoft.tfs.client.common.util.ConnectionHelper; import com.microsoft.tfs.core.TFSTeamProjectCollection; import com.microsoft.tfs.core.clients.build.IBuildDetail; import com.microsoft.tfs.core.clients.build.IQueuedBuild; import com.microsoft.tfs.core.clients.build.buildstatus.BuildStatusCache; import com.microsoft.tfs.core.clients.build.flags.BuildStatus; import com.microsoft.tfs.core.clients.build.flags.InformationTypes; import com.microsoft.tfs.core.clients.build.flags.QueryOptions; import com.microsoft.tfs.util.Check; import com.microsoft.tfs.util.listeners.SingleListenerFacade; /** * A {@link BuildStatusManager} polls the Team Foundation Server periodically to * update the status of builds. Listeners will be notified when the status of a * build has changed. This allows integration points with reconciliation of * workspaces for gated checkins. */ public class BuildStatusManager { public static final CodeMarker CODEMARKER_WATCHED_BUILD_REMOVED = new CodeMarker( "com.microsoft.tfs.client.common.server.cache.buildstatus.BuildStatusManager#watchedBuildRemoved"); //$NON-NLS-1$ private final Log log = LogFactory.getLog(BuildStatusManager.class); private final TFSTeamProjectCollection connection; private final Object watchedBuildLock = new Object(); private final Map<Integer, IQueuedBuild> watchedBuilds = new HashMap<Integer, IQueuedBuild>(); /* * Listeners who will be notified when a new build is being watched (or a * build is no longer being watched) or when a watched build's status * changes. */ private final SingleListenerFacade listeners = new SingleListenerFacade(BuildStatusManagerListener.class); private volatile int refreshInterval = (5 * 60 * 1000); private final Object refreshLock = new Object(); private boolean refreshInBackground = true; private boolean refreshInProgress = false; private long refreshLastTime = 0; /* Plugins may add build status listeners via extension points */ public static final String LISTENER_EXTENSION_POINT_ID = "com.microsoft.tfs.client.common.buildStatusManagerListeners"; //$NON-NLS-1$ public BuildStatusManager(final TFSTeamProjectCollection connection) { Check.notNull(connection, "connection"); //$NON-NLS-1$ this.connection = connection; loadConfiguration(); loadExtensionListeners(); final Thread refreshThread = new Thread(new BuildStatusManagerRefreshWorker()); refreshThread.setName("Build Status Manager"); //$NON-NLS-1$ refreshThread.start(); } /** * Sets fields from saved preferences. */ private void loadConfiguration() { /* * TODO Use a non-deprecated preference interface for this non-UI * plug-in. When Eclipse deprecated Preferences and * getPluginPreferences(), it put the modern equivalent only in * AbstractUIPlugin (which this plug-in isn't). We need to write our own * adapter or other preference layer and use that instead. */ final Preferences prefs = TFSCommonClientPlugin.getDefault().getPluginPreferences(); int refreshIntervalMillis = prefs.getInt(PreferenceConstants.BUILD_STATUS_REFRESH_INTERVAL); if (refreshIntervalMillis <= 0) { refreshIntervalMillis = prefs.getDefaultInt(PreferenceConstants.BUILD_STATUS_REFRESH_INTERVAL); } if (refreshIntervalMillis > 0) { refreshInterval = refreshIntervalMillis; } } private void loadExtensionListeners() { final IExtensionRegistry registry = Platform.getExtensionRegistry(); final IExtensionPoint extensionPoint = registry.getExtensionPoint(LISTENER_EXTENSION_POINT_ID); final IConfigurationElement[] elements = extensionPoint.getConfigurationElements(); for (int i = 0; i < elements.length; i++) { try { final BuildStatusManagerListener listener = (BuildStatusManagerListener) elements[0] .createExecutableExtension("class"); //$NON-NLS-1$ if (listener == null) { log.warn(MessageFormat.format("Could not create build status manager listener with id {0}", //$NON-NLS-1$ elements[0].getAttribute("id"))); //$NON-NLS-1$ } else { addListener(listener); } } catch (final CoreException e) { log.warn(MessageFormat.format("Could not create build status manager listener with id {0}", //$NON-NLS-1$ elements[0].getAttribute("id")), //$NON-NLS-1$ e); } } } public void setRefreshInterval(final int refreshInterval) { this.refreshInterval = refreshInterval; } public int getRefreshInterval() { return refreshInterval; } public void refresh() { synchronized (refreshLock) { if (refreshInProgress) { return; } refreshInProgress = true; } refreshInternal(); } /** * You must set refreshInProgress to true before calling this method, to * block other refreshes from running. */ private void refreshInternal() { synchronized (refreshLock) { Check.isTrue(refreshInProgress, "refreshInProgress"); //$NON-NLS-1$ } try { /* If we're not connected, defer. */ if (!ConnectionHelper.isConnected(connection)) { return; } /* * If the current server does not support queued builds at all (TFS * 2005, V1 service), do no work here. */ if (connection.getBuildServer().getBuildServerVersion().isV1()) { return; } /* A list of build changes to notify listeners for */ final List<IQueuedBuild> watchAddedList = new ArrayList<IQueuedBuild>(); final List<IQueuedBuild> watchRemovedList = new ArrayList<IQueuedBuild>(); final List<IQueuedBuild> statusChangedList = new ArrayList<IQueuedBuild>(); /* The list of watched build IDs to query from the server */ final List<Integer> queryIdList = new ArrayList<Integer>(); BuildStatusCache statusCache; /* Add in the in-memory list of watched builds */ synchronized (watchedBuildLock) { /* * Compose a list of build ids that have been removed from the * on-disk cache. (Ie, they are currently in memory, but are not * in the on-disk cache, meaning that another process has * removed this from the watch list.) Create the list by taking * all watched builds in-memory and removing those on disk. The * remainder is builds that were removed externally. */ final List<Integer> externallyRemovedList = new ArrayList<Integer>(); externallyRemovedList.addAll(watchedBuilds.keySet()); /* * Synchronize with the list of data on-disk (another process * may have modified this list.) Assume the build status cache * is the canonical list of watched build IDs. (It will always * include any modifications made in this client, or in any * other client.) */ statusCache = BuildStatusCache.load(connection); queryIdList.addAll(statusCache.getBuilds()); /* * Remove any items in our in-memory list that did not exist on * disk. */ externallyRemovedList.removeAll(queryIdList); for (final Iterator<Integer> i = externallyRemovedList.iterator(); i.hasNext();) { final Integer removedId = i.next(); log.debug(MessageFormat.format( "Queued build id {0} no longer watched, removed by an external process", //$NON-NLS-1$ ("" + removedId))); //$NON-NLS-1$ watchRemovedList.add(watchedBuilds.remove(removedId)); } } if (log.isDebugEnabled()) { if (queryIdList.size() == 0) { log.debug("No watched builds to query"); //$NON-NLS-1$ } else { final StringBuffer buildIdDebugList = new StringBuffer(); for (int i = 0; i < queryIdList.size(); i++) { buildIdDebugList.append(((i > 0) ? ", " : "") + queryIdList.get(i)); //$NON-NLS-1$ //$NON-NLS-2$ } log.debug("Querying build status for queued build ids: " + buildIdDebugList.toString()); //$NON-NLS-1$ } } if (queryIdList.size() > 0) { /* Query the server */ final int[] queryIds = new int[queryIdList.size()]; for (int i = 0; i < queryIdList.size(); i++) { queryIds[i] = queryIdList.get(i).intValue(); } /* * Query with definitions so that we can get the definition name * for the QueuedBuildsTableControl */ final IQueuedBuild[] queuedBuilds = connection.getBuildServer().getQueuedBuild(queryIds, QueryOptions.DEFINITIONS); final List<IQueuedBuild> savedQueuedBuilds = new ArrayList<IQueuedBuild>(); /* * Store new queued build data, see if the data has changed * since the last refresh */ synchronized (watchedBuildLock) { for (int i = 0; i < queuedBuilds.length; i++) { /* * The server no longer has this build detail. This * could be because the retention policy removed it or * it was removed manually. */ if (queuedBuilds[i].getID() == 0 || queuedBuilds[i].getBuild() == null && queuedBuilds[i].getStatus() == null) { log.debug(MessageFormat.format( "Watched build id {0} is no longer on the server, will no longer be watched", //$NON-NLS-1$ queryIds[i])); final IQueuedBuild existingQueuedBuildData = watchedBuilds.remove(queryIds[i]); if (existingQueuedBuildData != null) { watchRemovedList.add(existingQueuedBuildData); } continue; } savedQueuedBuilds.add(queuedBuilds[i]); final IQueuedBuild existingQueuedBuildData = watchedBuilds.get(queuedBuilds[i].getID()); /* * We do not have existing data, this was added by * another process */ if (existingQueuedBuildData == null) { watchAddedList.add(queuedBuilds[i]); watchedBuilds.put(queuedBuilds[i].getID(), queuedBuilds[i]); } /* * If the build status has changed, notify listeners and * update the cache with the newest data. */ else if (buildStatusChanged(existingQueuedBuildData, queuedBuilds[i])) { statusChangedList.add(queuedBuilds[i]); watchedBuilds.put(queuedBuilds[i].getID(), queuedBuilds[i]); } } } /* * Load the checkin details build information node for any newly * added or status changed builds who have completed building. */ final List<IQueuedBuild> loadInformationList = new ArrayList<IQueuedBuild>(); loadInformationList.addAll(watchAddedList); loadInformationList.addAll(statusChangedList); for (final IQueuedBuild queuedBuild : loadInformationList) { if (queuedBuild.getBuild() == null || queuedBuild.getBuild().getStatus() == null) { continue; } final IBuildDetail buildDetail = queuedBuild.getBuild(); final BuildStatus buildStatus = buildDetail.getStatus(); if ((buildStatus.contains(BuildStatus.PARTIALLY_SUCCEEDED) || buildStatus.contains(BuildStatus.SUCCEEDED)) && (buildDetail.getInformation() == null || buildDetail.getInformation() .getNodesByType(InformationTypes.CHECK_IN_OUTCOME).length == 0)) { buildDetail.refresh(new String[] { InformationTypes.CHECK_IN_OUTCOME }, QueryOptions.ALL); } } /* Persist changes to disk */ statusCache.setBuilds(savedQueuedBuilds.toArray(new IQueuedBuild[savedQueuedBuilds.size()])); statusCache.save(connection); } /* Notify listeners of new / removed watched builds */ if (statusChangedList.size() > 0 || watchAddedList.size() > 0 || watchRemovedList.size() > 0) { ((BuildStatusManagerListener) listeners.getListener()).onUpdateStarted(); for (final IQueuedBuild removedBuild : watchRemovedList) { ((BuildStatusManagerListener) listeners.getListener()).onWatchedBuildRemoved(removedBuild); } for (final IQueuedBuild addedBuild : watchAddedList) { ((BuildStatusManagerListener) listeners.getListener()).onWatchedBuildAdded(addedBuild); } for (final IQueuedBuild changedBuild : statusChangedList) { ((BuildStatusManagerListener) listeners.getListener()).onBuildStatusChanged(changedBuild); } ((BuildStatusManagerListener) listeners.getListener()).onUpdateFinished(); } } finally { synchronized (refreshLock) { refreshInProgress = false; refreshLastTime = System.currentTimeMillis(); } } } private boolean buildStatusChanged(final IQueuedBuild existingQueuedBuild, final IQueuedBuild newQueuedBuild) { Check.notNull(existingQueuedBuild, "existingQueuedBuild"); //$NON-NLS-1$ Check.notNull(newQueuedBuild, "newQueuedBuild"); //$NON-NLS-1$ /* If the build has not started, see if the queue status has changed */ if (existingQueuedBuild.getBuild() == null && newQueuedBuild.getBuild() == null) { return (!existingQueuedBuild.getStatus().equals(newQueuedBuild.getStatus())); } /* * The build has started (an IBuildDetail exists in the new queued build * but not our cached data) */ if (existingQueuedBuild.getBuild() == null && newQueuedBuild.getBuild() != null) { return true; } /* Otherwise, compare the old status with the new */ return (!existingQueuedBuild.getBuild().getStatus().equals(newQueuedBuild.getBuild().getStatus())); } public boolean addWatchedBuild(final IQueuedBuild build) { Check.notNull(build, "build"); //$NON-NLS-1$ boolean newWatchedBuild; synchronized (watchedBuildLock) { newWatchedBuild = (watchedBuilds.put(build.getID(), build) == null); } /* Only notify listeners if this build was not already being watched. */ if (newWatchedBuild == true) { final BuildStatusCache statusCache = BuildStatusCache.load(connection); statusCache.addBuild(build); statusCache.save(connection); ((BuildStatusManagerListener) listeners.getListener()).onWatchedBuildAdded(build); } return newWatchedBuild; } /** * Removes the given {@link IBuildDetail} from the watched build list. This * is not guaranteed to succeed, as * * @param buildDetail * @return */ public boolean removeWatchedBuild(final IBuildDetail buildDetail) { Check.notNull(buildDetail, "buildDetail"); //$NON-NLS-1$ /* * Unfortunately, there's nothing tying an IBuildDetail to an * IQueuedBuild, so we have to go through the IQueuedBuilds to see if we * already have an IBuildDetail for them and get the queue id that way. */ int buildId = -1; synchronized (watchedBuildLock) { for (final IQueuedBuild queuedBuild : watchedBuilds.values()) { if (queuedBuild.getBuild() == null) { continue; } if (buildDetail.getBuildNumber().equals(queuedBuild.getBuild().getBuildNumber())) { buildId = queuedBuild.getID(); break; } } } if (buildId >= 0) { return removeWatchedBuild(buildId); } return false; } public boolean removeWatchedBuild(final IQueuedBuild build) { Check.notNull(build, "build"); //$NON-NLS-1$ return removeWatchedBuild(build.getID()); } public boolean removeWatchedBuild(final int id) { IQueuedBuild existingWatchedBuild; synchronized (watchedBuildLock) { existingWatchedBuild = watchedBuilds.remove(id); } /* Only notify listeners if this build was being watched. */ if (existingWatchedBuild != null) { final BuildStatusCache statusCache = BuildStatusCache.load(connection); statusCache.removeBuild(id); statusCache.save(connection); ((BuildStatusManagerListener) listeners.getListener()).onWatchedBuildRemoved(existingWatchedBuild); CodeMarkerDispatch.dispatch(CODEMARKER_WATCHED_BUILD_REMOVED); return true; } else { return false; } } public IQueuedBuild[] getWatchedBuilds() { synchronized (watchedBuildLock) { return watchedBuilds.values().toArray(new IQueuedBuild[watchedBuilds.size()]); } } /** * Stops the build status manager. Should be called when disconnecting from * a server. */ public void stop() { synchronized (refreshLock) { refreshInBackground = false; } } public void addListener(final BuildStatusManagerListener listener) { Check.notNull(listener, "listener"); //$NON-NLS-1$ listeners.addListener(listener); } public void removeListener(final BuildStatusManagerListener listener) { Check.notNull(listener, "listener"); //$NON-NLS-1$ listeners.removeListener(listener); } private class BuildStatusManagerRefreshWorker implements Runnable { private final Log log = LogFactory.getLog(BuildStatusManagerRefreshWorker.class); @Override public void run() { log.info("Starting build status refresh worker"); //$NON-NLS-1$ while (true) { boolean doRefresh = false; long sleepTime = 0; synchronized (refreshLock) { /* * The user may have cancelled this background job. This * could be because the server is now invalid * (disconnected.) */ if (refreshInBackground == false) { log.info("Stopping build status refresh worker"); //$NON-NLS-1$ break; } /* * If another refresh is not in progress and we're past time * for a refresh, then schedule one. */ final long currentTime = System.currentTimeMillis(); if (refreshInProgress) { /* * If there's a refresh going on (manual refresh) defer * until the refresh interval and check again */ sleepTime = refreshInterval; } else if (refreshLastTime + refreshInterval <= currentTime) { /* Otherwise, it's time to do a refresh */ doRefresh = true; refreshInProgress = true; sleepTime = refreshInterval; } else { /* * Otherwise, we got woken up before a refresh was due, * simply sleep until we think it's time to do the * refresh */ sleepTime = (refreshLastTime + refreshInterval) - currentTime; } } if (doRefresh) { startRefreshJob(); } try { Thread.sleep(sleepTime); } catch (final InterruptedException e) { log.warn("Build status refresh worker interrupted", e); //$NON-NLS-1$ break; } } } private void startRefreshJob() { final Job refreshJob = new Job(Messages.getString("BuildStatusManager.BuildStatusRefreshJobName")) //$NON-NLS-1$ { @Override protected IStatus run(final IProgressMonitor monitor) { try { refreshInternal(); } catch (final Exception e) { log.debug("Exception during auto-refresh.", e); //$NON-NLS-1$ } return Status.OK_STATUS; } }; refreshJob.schedule(); } } }