Index: main/src/net/sourceforge/cruisecontrol/BuildQueue.java
===================================================================
RCS file: /cvsroot/cruisecontrol/cruisecontrol/main/src/net/sourceforge/cruisecontrol/BuildQueue.java,v
retrieving revision 1.12
diff -u -r1.12 BuildQueue.java
--- main/src/net/sourceforge/cruisecontrol/BuildQueue.java 3 Jul 2004 05:54:34 -0000 1.12
+++ main/src/net/sourceforge/cruisecontrol/BuildQueue.java 15 Jun 2005 14:01:31 -0000
@@ -55,7 +55,7 @@
private LinkedList queue = new LinkedList();
private boolean waiting = false;
- private Thread buildQueueThread;
+ private Thread buildQueueThread = null;
/**
* @param project
@@ -74,15 +74,16 @@
nextProject = (Project) queue.remove(0);
}
if (nextProject != null) {
- LOG.info("now adding to the thread queue: " + nextProject.getName());
ProjectWrapper pw = new ProjectWrapper(nextProject);
// let's not add the task more than once
String name = nextProject.getName();
if (ThreadQueue.isActive(name)) {
+ LOG.info("already in the thread queue: " + nextProject.getName());
// it's already there... don't re-add it.
// later, we'll need to add it to a queued up list
// so we don't 'forget' about the new build request
} else {
+ LOG.info("now adding to the thread queue: " + nextProject.getName());
ThreadQueue.addTask(pw);
}
}
@@ -129,10 +130,12 @@
}
void stop() {
- LOG.info("Stopping BuildQueue");
- buildQueueThread.interrupt();
- synchronized (queue) {
- queue.notify();
+ if (buildQueueThread != null) {
+ LOG.info("Stopping BuildQueue");
+ buildQueueThread.interrupt();
+ synchronized (queue) {
+ queue.notify();
+ }
}
}
@@ -143,4 +146,9 @@
public boolean isWaiting() {
return waiting;
}
+
+ public boolean empty() {
+ return queue.size() == 0;
+ }
+
}
Index: main/src/net/sourceforge/cruisecontrol/CruiseControlController.java
===================================================================
RCS file: /cvsroot/cruisecontrol/cruisecontrol/main/src/net/sourceforge/cruisecontrol/CruiseControlController.java,v
retrieving revision 1.12
diff -u -r1.12 CruiseControlController.java
--- main/src/net/sourceforge/cruisecontrol/CruiseControlController.java 17 Dec 2004 16:59:33 -0000 1.12
+++ main/src/net/sourceforge/cruisecontrol/CruiseControlController.java 15 Jun 2005 14:01:31 -0000
@@ -44,10 +44,17 @@
import java.io.FileInputStream;
import java.io.ObjectInputStream;
import java.util.ArrayList;
+import java.util.Collection;
import java.util.Collections;
import java.util.EventListener;
+import java.util.HashMap;
+import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.TimerTask;
+import java.util.Timer;
/**
*
@@ -57,17 +64,52 @@
private static final Logger LOG = Logger.getLogger(CruiseControlController.class);
public static final String DEFAULT_CONFIG_FILE_NAME = "config.xml";
private File configFile;
- private List projects = new ArrayList();
- private BuildQueue buildQueue = new BuildQueue();
-
- private List listeners = new ArrayList();
+ private Map projectsMap = new HashMap();
+ private final BuildQueue buildQueue = new BuildQueue();
+ private ConfigReloader configReloader = null;
+ private Object projectsLock = new Object();
+
+ protected void setConfigReloader(ConfigReloader configReloader) {
+ this.configReloader = configReloader;
+ configReloader.start(); // TV
+ }
+
+ interface ConfigReloader {
+ void start();
+ }
+
+ class TimerBasedConfigReloader implements ConfigReloader {
+ private final Timer timer = new Timer(true);
+ private TimerTask configReloadTask = null;
+
+ public synchronized void start() {
+ if (configReloadTask == null) {
+ configReloadTask = new TimerTask() {
+ public void run() {
+ parseConfigFileIfNecessary();
+ }
+ };
+ timer.schedule(configReloadTask, timeBetweenConfigReload, timeBetweenConfigReload);
+ }
+ }
+ }
+
+ private long lastConfigLoadTime = 0;
+ private long timeBetweenConfigReload = 10 * 1000;
+
+ private final List listeners = new ArrayList();
public File getConfigFile() {
return configFile;
}
+ public void setTimeBetweenConfigReload(long time) {
+ timeBetweenConfigReload = time;
+ }
+
public void setConfigFile(File configFile) throws CruiseControlException {
this.configFile = configFile;
+ lastConfigLoadTime = 0;
if (configFile == null) {
throw new CruiseControlException("No config file");
}
@@ -75,16 +117,74 @@
throw new CruiseControlException("Config file not found: " + configFile.getName());
}
parseConfigFile();
+ setConfigReloader(new TimerBasedConfigReloader()); // TV
+ //configReloader.start(); // TV
+ }
+
+ public void parseConfigFileIfNecessary() {
+ // FIXME doesn't handle when changes happen twice in same second...
+ long lastModified = configFile.lastModified();
+ if (lastModified == lastConfigLoadTime) { // TV : <= was risky when the timestamp comes from another machine
+ return;
+ };
+ LOG.info("Parsing configuration file. File modified " + lastModified + " since " + lastConfigLoadTime);
+ pause(); // TV : yes!
+ try {
+ parseConfigFile();
+ } catch (CruiseControlException e) {
+ // FIXME how do we handle failures? Should we stop and wait for a fix?
+ LOG.error("Failure to reload config file \n" + e.getMessage());
+ }
+ resume(); // TV : yes!
+ notifyConfigChange(); // should we notify on first read?
+ }
+
+ private void notifyConfigChange() {
+ for (Iterator listenIter = listeners.iterator(); listenIter.hasNext();) {
+ LOG.debug("Informing listener of config change");
+ Listener listener = (Listener) listenIter.next();
+ listener.configChanged();
+ }
+ }
+
+ static void copySet(Set source, Set dest) {
+ final Iterator iterator = source.iterator();
+ while (iterator.hasNext()) {
+ dest.add(iterator.next());
+ }
}
private void parseConfigFile() throws CruiseControlException {
Element configRoot = Util.loadConfigFile(configFile);
addPluginsToRootRegistry(configRoot);
- List projectList = getAllProjects(configRoot);
- for (Iterator iterator = projectList.iterator(); iterator.hasNext();) {
- Project project = (Project) iterator.next();
- addProject(project);
+ Set oldProjectNames = new HashSet();
+ copySet(projectsMap.keySet(), oldProjectNames);
+ List newProjectList = getAllProjects(configRoot);
+ synchronized (projectsLock) {
+ for (Iterator iterator = newProjectList.iterator(); iterator.hasNext();) {
+ Project project = (Project) iterator.next();
+ final String name = project.getName();
+ if (projectsMap.containsKey(name)) {
+ boolean removed = oldProjectNames.remove(name);
+ if (!removed) {
+ throw new CruiseControlException("Two projects with identical name " + name);
+ }
+ } else {
+ addProject(project);
+ project.init(); // TV : delayed init.
+ }
+ }
+
+ // all non treated projects have been removed
+ Iterator it = oldProjectNames.iterator();
+ while (it.hasNext()) {
+ String projectName = (String) it.next();
+ Project project = (Project) projectsMap.get(projectName);
+ removeProject(project);
+ project.stop();
+ }
}
+ lastConfigLoadTime = configFile.lastModified();
}
private void addPluginsToRootRegistry(Element configRoot) {
@@ -104,7 +204,10 @@
}
private void addProject(Project project) {
- projects.add(project);
+ LOG.info("Adding project " + project.getName());
+ synchronized (projectsLock) {
+ projectsMap.put(project.getName(), project);
+ }
for (Iterator listenIter = listeners.iterator(); listenIter.hasNext();) {
LOG.debug("Informing listener of new project");
Listener listener = (Listener) listenIter.next();
@@ -112,20 +215,36 @@
}
}
+ private void removeProject(Project project) {
+ LOG.info("Removing project " + project.getName());
+ synchronized (projectsLock) {
+ projectsMap.remove(project.getName());
+ }
+ for (Iterator listenIter = listeners.iterator(); listenIter.hasNext();) {
+ LOG.debug("Informing listener of removed project");
+ Listener listener = (Listener) listenIter.next();
+ listener.projectRemoved(project);
+ }
+ }
+
public void resume() {
buildQueue.start();
- for (Iterator iterator = projects.iterator(); iterator.hasNext();) {
- Project currentProject = (Project) iterator.next();
- currentProject.setBuildQueue(buildQueue);
- currentProject.start();
+ synchronized (projectsLock) {
+ for (Iterator iterator = projectsMap.values().iterator(); iterator.hasNext();) {
+ Project currentProject = (Project) iterator.next();
+ currentProject.setBuildQueue(buildQueue);
+ currentProject.start();
+ }
}
}
public void pause() {
buildQueue.stop();
- for (Iterator iterator = projects.iterator(); iterator.hasNext();) {
- Project currentProject = (Project) iterator.next();
- currentProject.stop();
+ synchronized (projectsLock) {
+ for (Iterator iterator = projectsMap.values().iterator(); iterator.hasNext();) {
+ Project currentProject = (Project) iterator.next();
+ currentProject.stop();
+ }
}
}
@@ -147,12 +266,17 @@
}
public List getProjects() {
- return Collections.unmodifiableList(projects);
+ final Collection c;
+ synchronized (projectsLock) {
+ c = projectsMap.values();
+ }
+ return Collections.unmodifiableList(new ArrayList(c));
}
+ // TV : getAllProjects no more returns init'ed Projects
private List getAllProjects(Element configRoot) throws CruiseControlException {
String[] projectNames = getProjectNames(configRoot);
- ArrayList allProjects = new ArrayList(projectNames.length);
+ List allProjects = new ArrayList(projectNames.length);
for (int i = 0; i < projectNames.length; i++) {
String projectName = projectNames[i];
LOG.info("projectName = [" + projectName + "]");
@@ -165,8 +289,8 @@
protected Project configureProject(String projectName) throws CruiseControlException {
Project project = readProject(projectName);
project.setName(projectName);
- project.setConfigFile(configFile);
- project.init();
+ project.setConfigFile(configFile);
+ // project.init(); // TV : delay init
return project;
}
@@ -231,5 +355,7 @@
public static interface Listener extends EventListener {
void projectAdded(Project project);
+ void projectRemoved(Project project);
+ void configChanged();
}
}
Index: main/src/net/sourceforge/cruisecontrol/Main.java
===================================================================
RCS file: /cvsroot/cruisecontrol/cruisecontrol/main/src/net/sourceforge/cruisecontrol/Main.java,v
retrieving revision 1.39
diff -u -r1.39 Main.java
--- main/src/net/sourceforge/cruisecontrol/Main.java 14 Mar 2005 22:47:36 -0000 1.39
+++ main/src/net/sourceforge/cruisecontrol/Main.java 15 Jun 2005 14:01:31 -0000
@@ -93,6 +93,13 @@
agent.start();
}
controller.resume();
+
+ // TV : don't exit the main thread!
+ while (true) {
+ try { Thread.sleep(10000); }
+ catch (InterruptedException e) { }
+ }
+
} catch (CruiseControlException e) {
LOG.fatal(e.getMessage());
usage();
Index: main/src/net/sourceforge/cruisecontrol/Project.java
===================================================================
RCS file: /cvsroot/cruisecontrol/cruisecontrol/main/src/net/sourceforge/cruisecontrol/Project.java,v
retrieving revision 1.93
diff -u -r1.93 Project.java
--- main/src/net/sourceforge/cruisecontrol/Project.java 11 Jun 2005 23:29:23 -0000 1.93
+++ main/src/net/sourceforge/cruisecontrol/Project.java 15 Jun 2005 14:01:31 -0000
@@ -90,6 +90,7 @@
private transient Object pausedMutex;
private transient Object scheduleMutex;
private transient Object waitMutex;
+ private transient Object terminationMutex;
private transient BuildQueue queue;
private transient List progressListeners;
private transient List resultListeners;
@@ -105,7 +106,7 @@
private String buildTarget = null;
private boolean isPaused = false;
private boolean buildAfterFailed = true;
- private transient Thread projectSchedulingThread;
+ private transient Thread projectSchedulingThread = null;
private boolean stopped = true;
public Project() {
@@ -124,6 +125,7 @@
pausedMutex = new Object();
scheduleMutex = new Object();
waitMutex = new Object();
+ terminationMutex = new Object();
progressListeners = new ArrayList();
resultListeners = new ArrayList();
}
@@ -248,6 +250,9 @@
try {
waitIfPaused();
waitForNextBuild();
+ if (stopped) {
+ return; // TV
+ }
setState(ProjectState.QUEUED);
synchronized (scheduleMutex) {
queue.requestBuild(this);
@@ -260,7 +265,10 @@
}
}
} finally {
- stopped = true;
+ synchronized (terminationMutex) {
+ stopped = true;
+ projectSchedulingThread = null;
+ }
LOG.info("Project " + name + " stopped");
}
}
@@ -737,9 +745,28 @@
LOG.debug("Project " + name + ": " + message);
}
- public void start() {
- stopped = false;
- projectSchedulingThread = new Thread(this, "Project " + getName() + " thread");
+ public void start() { // TV : manage Project restart
+ Thread t = null;
+ synchronized (terminationMutex) {
+ t = projectSchedulingThread;
+ }
+ if (t != null) {
+ forceBuild();
+ try {
+ while (!queue.empty()) {
+ Thread.sleep(100);
+ }
+ t.join();
+ } catch (InterruptedException e) {
+ String message = "Project " + name + ".start() interrupted";
+ LOG.error(message, e);
+ throw new RuntimeException(message);
+ }
+ }
+ synchronized (terminationMutex) {
+ stopped = false;
+ projectSchedulingThread = new Thread(this, "Project " + getName() + " thread");
+ }
projectSchedulingThread.start();
LOG.info("Project " + name + " starting");
setState(ProjectState.IDLE);
@@ -747,7 +774,9 @@
public void stop() {
LOG.info("Project " + name + " stopping");
- stopped = true;
+ synchronized (terminationMutex) {
+ stopped = true;
+ }
setState(ProjectState.STOPPED);
}
Index: main/src/net/sourceforge/cruisecontrol/jmx/CruiseControlControllerJMXAdaptor.java
===================================================================
RCS file: /cvsroot/cruisecontrol/cruisecontrol/main/src/net/sourceforge/cruisecontrol/jmx/CruiseControlControllerJMXAdaptor.java,v
retrieving revision 1.7
diff -u -r1.7 CruiseControlControllerJMXAdaptor.java
--- main/src/net/sourceforge/cruisecontrol/jmx/CruiseControlControllerJMXAdaptor.java 1 Jun 2005 20:49:57 -0000 1.7
+++ main/src/net/sourceforge/cruisecontrol/jmx/CruiseControlControllerJMXAdaptor.java 15 Jun 2005 14:01:31 -0000
@@ -122,4 +122,23 @@
LOG.error("Could not register project " + project.getName(), e);
}
}
+
+ public void projectRemoved(Project project) {
+ // FIXME implement
+/*
+ try {
+ LOG.debug("Unregistering project mbean");
+ ProjectController projectController = new ProjectController(project);
+ projectController.unregister(server);
+ } catch (JMException e) {
+ LOG.error("Could not register project " + project.getName(), e);
+ }
+*/
+ }
+
+
+ public void configChanged() {
+ LOG.info("config changed");
+ }
+
}
Index: main/test/net/sourceforge/cruisecontrol/CruiseControlControllerTest.java
===================================================================
RCS file: /cvsroot/cruisecontrol/cruisecontrol/main/test/net/sourceforge/cruisecontrol/CruiseControlControllerTest.java,v
retrieving revision 1.10
diff -u -r1.10 CruiseControlControllerTest.java
--- main/test/net/sourceforge/cruisecontrol/CruiseControlControllerTest.java 1 Jun 2005 20:54:49 -0000 1.10
+++ main/test/net/sourceforge/cruisecontrol/CruiseControlControllerTest.java 15 Jun 2005 14:01:31 -0000
@@ -41,6 +41,8 @@
import java.io.File;
import java.io.FileWriter;
import java.io.IOException;
+import java.util.List;
+import java.util.ArrayList;
/**
*
@@ -51,8 +53,14 @@
private File configFile = new File("_tempConfigFile");
private CruiseControlController ccController;
+ class DummyConfigReloader implements CruiseControlController.ConfigReloader {
+ public void start() {
+ }
+ }
+
protected void setUp() throws Exception {
ccController = new CruiseControlController();
+ ccController.setConfigReloader(new DummyConfigReloader());
}
public void tearDown() {
@@ -94,6 +102,7 @@
protected Project configureProject(String projectName) {
final Project project = new Project();
project.setName(projectName);
+ project.setConfigFile(configFile);
return project;
}
};
@@ -110,6 +119,135 @@
assertEquals(2, ccController.getProjects().size());
}
+ public void testLoadSomeProjectsWithDuplicates() throws IOException, CruiseControlException {
+ ccController = new CruiseControlController() {
+ protected Project configureProject(String projectName) {
+ final Project project = new Project();
+ project.setName(projectName);
+ project.setConfigFile(configFile);
+ return project;
+ }
+ };
+ FileWriter configOut = new FileWriter(configFile);
+ configOut.write("\n");
+ configOut.write("\n");
+ writeProjectDetails(configOut, "testProject1");
+ writeProjectDetails(configOut, "testProject1");
+ configOut.write("\n");
+ configOut.close();
+
+ try {
+ ccController.setConfigFile(configFile);
+ fail("duplicate project names should fail");
+ } catch (CruiseControlException e) {
+ // expected
+ }
+ }
+
+ public void testConfigReloading() throws IOException, CruiseControlException {
+ class MyListener implements CruiseControlController.Listener {
+ List addedProjects = new ArrayList();
+ List removedProjects = new ArrayList();
+ int configReloads = 0;
+ public void clear() {
+ addedProjects.clear();
+ removedProjects.clear();
+ configReloads = 0;
+ }
+ public void projectAdded(Project project) {
+ addedProjects.add(project);
+ }
+ public void projectRemoved(Project project) {
+ removedProjects.add(project);
+ }
+ public void configChanged() { configReloads++; }
+ };
+ MyListener listener = new MyListener();
+
+ ccController = new CruiseControlController() {
+ protected Project configureProject(String projectName) {
+ final Project project = new Project();
+ project.setName(projectName);
+ project.setConfigFile(configFile);
+ return project;
+ }
+ };
+ ccController.addListener(listener);
+ FileWriter configOut = new FileWriter(configFile);
+ configOut.write("\n");
+ configOut.write("\n");
+ writeProjectDetails(configOut, "testProject1");
+ writeProjectDetails(configOut, "testProject2");
+ configOut.write("\n");
+ configOut.close();
+ ccController.setConfigFile(configFile);
+
+ assertEquals(configFile, ccController.getConfigFile());
+ assertEquals(2, ccController.getProjects().size());
+ assertEquals(2, listener.addedProjects.size());
+ assertEquals(0, listener.removedProjects.size());
+ assertEquals(0, listener.configReloads);
+
+ listener.clear();
+
+ // no change - no reload
+ ccController.parseConfigFileIfNecessary();
+ // nothing happened
+ assertEquals(0, listener.configReloads);
+ assertEquals(0, listener.addedProjects.size());
+ assertEquals(0, listener.removedProjects.size());
+
+
+ // add a project:
+
+ listener.clear();
+
+ sleep(1200);
+ configOut = new FileWriter(configFile);
+ configOut.write("\n");
+ configOut.write("\n");
+ writeProjectDetails(configOut, "testProject1");
+ writeProjectDetails(configOut, "testProject2");
+ writeProjectDetails(configOut, "testProject3");
+ configOut.write("\n");
+ configOut.close();
+
+ ccController.parseConfigFileIfNecessary();
+
+ assertEquals(1, listener.configReloads);
+ assertEquals(3, ccController.getProjects().size());
+ assertEquals(1, listener.addedProjects.size());
+ assertEquals(0, listener.removedProjects.size());
+
+ // remove 2 projects
+
+ listener.clear();
+
+ sleep(1200);
+ configOut = new FileWriter(configFile);
+ configOut.write("\n");
+ configOut.write("\n");
+ writeProjectDetails(configOut, "testProject3");
+ configOut.write("\n");
+ configOut.close();
+
+ ccController.parseConfigFileIfNecessary();
+
+ assertEquals(1, listener.configReloads);
+ assertEquals(1, ccController.getProjects().size());
+ assertEquals(0, listener.addedProjects.size());
+ assertEquals(2, listener.removedProjects.size());
+
+ }
+
+ private void sleep(long l) {
+ try {
+ Thread.sleep(l);
+ } catch (InterruptedException e) {
+
+ }
+ }
+
public void testReadProject() throws IOException {
File tempFile = File.createTempFile("foo", ".tmp");
String tempDir = tempFile.getParent();
@@ -137,6 +275,9 @@
}
private void writeProjectDetails(FileWriter configOut, final String projectName) throws IOException {
- configOut.write("\n");
+ configOut.write("\n");
+ configOut.write("\n");
+ configOut.write("\n");
+ configOut.write("\n");
}
}