Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ Manifest-Version: 1.0
Bundle-ManifestVersion: 2
Bundle-Name: %pluginName
Bundle-SymbolicName: org.eclipse.core.resources; singleton:=true
Bundle-Version: 3.24.0.qualifier
Bundle-Version: 3.25.0.qualifier
Bundle-Activator: org.eclipse.core.resources.ResourcesPlugin
Bundle-Vendor: %providerName
Bundle-Localization: plugin
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@
import org.eclipse.core.resources.IProject;
import org.eclipse.core.resources.IProjectDescription;
import org.eclipse.core.resources.IResource;
import org.eclipse.core.resources.IResourceChangeEvent;
import org.eclipse.core.resources.IResourceDelta;
import org.eclipse.core.resources.IResourceStatus;
import org.eclipse.core.resources.IncrementalProjectBuilder;
Expand Down Expand Up @@ -262,6 +263,11 @@ private void basicBuild(int trigger, IncrementalProjectBuilder builder, Map<Stri
currentTree = ((trigger == IncrementalProjectBuilder.FULL_BUILD) || clean) ? null : workspace.getElementTree();
int depth = -1;
ISchedulingRule rule = null;
IProject builderProject = builder.getProject();
ICommand builderCommand = builder.getCommand();
String builderName = builderCommand != null ? builderCommand.getBuilderName() : null;
workspace.broadcastProjectBuildEvent(builderProject, builderName,
IResourceChangeEvent.PRE_PROJECT_BUILD, trigger);
Comment on lines +266 to +270
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

Potential NullPointerException if builder.getCommand() returns null. According to the IncrementalProjectBuilder#getCommand() documentation, the returned value may be null if the builder was not created from a build command. A defensive check should be added before accessing the builder name to ensure the build process is not interrupted by an unexpected NPE.

Suggested change
IProject builderProject = builder.getProject();
String builderName = builder.getCommand().getBuilderName();
workspace.broadcastProjectBuildEvent(builderProject, builderName,
IResourceChangeEvent.PRE_PROJECT_BUILD, trigger);
IProject builderProject = builder.getProject();
String builderName = builder.getCommand() != null ? builder.getCommand().getBuilderName() : null;
workspace.broadcastProjectBuildEvent(builderProject, builderName,
IResourceChangeEvent.PRE_PROJECT_BUILD, trigger);

try {
//short-circuit if none of the projects this builder cares about have changed.
if (!needsBuild(currentBuilder, trigger)) {
Expand All @@ -274,9 +280,9 @@ private void basicBuild(int trigger, IncrementalProjectBuilder builder, Map<Stri
String name = currentBuilder.getLabel();
String message;
if (name != null) {
message = NLS.bind(Messages.events_invoking_2, name, builder.getProject().getFullPath());
message = NLS.bind(Messages.events_invoking_2, name, builderProject.getFullPath());
} else {
message = NLS.bind(Messages.events_invoking_1, builder.getProject().getFullPath());
message = NLS.bind(Messages.events_invoking_1, builderProject.getFullPath());
}
monitor.subTask(message);
hookStartBuild(builder, trigger);
Expand All @@ -302,7 +308,9 @@ private void basicBuild(int trigger, IncrementalProjectBuilder builder, Map<Stri
//do the build
SafeRunner.run(getSafeRunnable(currentBuilder, trigger, args, status, monitor));
} finally {
// Re-acquire the WS lock, then release the scheduling rule
// Re-acquire the WS lock, then release the scheduling rule, so
// POST_PROJECT_BUILD listeners see the same locking state as
// PRE_PROJECT_BUILD listeners did: WS lock held, no rule held.
if (depth >= 0) {
getWorkManager().endUnprotected(depth);
}
Expand All @@ -313,23 +321,28 @@ private void basicBuild(int trigger, IncrementalProjectBuilder builder, Map<Stri
throw handleRuleConflict(false, currentBuilder, e);
}
}
// Be sure to clean up after ourselves.
if (clean || currentBuilder.wasForgetStateRequested()) {
currentBuilder.setLastBuiltTree(null);
} else if (currentBuilder.wasRememberStateRequested()) {
// If remember last build state, and FULL_BUILD
// last tree must be set to => null for next build
if (trigger == IncrementalProjectBuilder.FULL_BUILD) {
try {
workspace.broadcastProjectBuildEvent(builderProject, builderName,
IResourceChangeEvent.POST_PROJECT_BUILD, trigger);
} finally {
Comment on lines +324 to +327
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The POST_PROJECT_BUILD event is currently fired before the workspace lock is re-acquired (via endUnprotected). This violates the general contract of IResourceChangeEvent which states that the workspace is closed for changes during notification. Since PRE_PROJECT_BUILD is fired with the lock held, POST_PROJECT_BUILD should ideally also be fired after endUnprotected to ensure consistency and that listeners see a locked workspace, unless the concurrent delivery while the lock is released is explicitly intended for performance reasons.

// Be sure to clean up after ourselves.
if (clean || currentBuilder.wasForgetStateRequested()) {
currentBuilder.setLastBuiltTree(null);
} else if (currentBuilder.wasRememberStateRequested()) {
// If remember last build state, and FULL_BUILD
// last tree must be set to => null for next build
if (trigger == IncrementalProjectBuilder.FULL_BUILD) {
currentBuilder.setLastBuiltTree(null);
}
// else don't modify the last built tree
} else {
// remember the current state as the last built state.
ElementTree lastTree = workspace.getElementTree();
lastTree.immutable();
currentBuilder.setLastBuiltTree(lastTree);
}
// else don't modify the last built tree
} else {
// remember the current state as the last built state.
ElementTree lastTree = workspace.getElementTree();
lastTree.immutable();
currentBuilder.setLastBuiltTree(lastTree);
hookEndBuild(builder);
}
hookEndBuild(builder);
}
} finally {
currentBuilders.remove(currentBuilder);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -214,6 +214,24 @@ private void cleanUp(ElementTree lastState, int type) {
}
}

/**
* Dispatches a per-builder build event (PRE_PROJECT_BUILD / POST_PROJECT_BUILD).
* <p>
* Unlike {@link #broadcastChanges}, this path does not compute a resource
* delta, does not coalesce against the last build tree, and does not update
* {@code lastPostBuildTree}. Per-builder events are pure timing/metadata
* signals nested inside the surrounding workspace-level PRE_BUILD/POST_BUILD
* pair, which continues to carry the aggregated delta.
* </p>
*/
public void broadcastProjectBuildEvent(IProject project, String builderName, int type, int buildKind) {
if (!listeners.hasListenerFor(type)) {
return;
}
ResourceChangeEvent event = new ResourceChangeEvent(project, type, buildKind, project, builderName);
notify(getListeners(), event, false);
}

/**
* Helper method for the save participant lifecycle computation. */
public void broadcastChanges(IResourceChangeListener listener, int type, IResourceDelta delta) {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*******************************************************************************
* Copyright (c) 2000, 2015 IBM Corporation and others.
* Copyright (c) 2000, 2026 IBM Corporation and others.
*
* This program and the accompanying materials
* are made available under the terms of the Eclipse Public License 2.0
Expand All @@ -12,6 +12,7 @@
* IBM Corporation - initial API and implementation
* James Blackburn (Broadcom Corp.) - ongoing development
* Lars Vogel <Lars.Vogel@vogella.com> - Bug 473427
* Vogella GmbH - per-builder build events
*******************************************************************************/
package org.eclipse.core.internal.events;

Expand All @@ -32,6 +33,11 @@ public class ResourceChangeEvent extends EventObject implements IResourceChangeE
private int trigger = 0;
int type;

/**
* The builder id for PRE_PROJECT_BUILD / POST_PROJECT_BUILD events, or null.
*/
private String builderName;

protected ResourceChangeEvent(Object source, int type, IResource resource) {
super(source);
this.resource = resource;
Expand All @@ -45,6 +51,12 @@ public ResourceChangeEvent(Object source, int type, int buildKind, IResourceDelt
this.type = type;
}

public ResourceChangeEvent(Object source, int type, int buildKind, IResource resource, String builderName) {
this(source, type, resource);
this.trigger = buildKind;
this.builderName = builderName;
}

/**
* @see IResourceChangeEvent#findMarkerDeltas(String, boolean)
*/
Expand Down Expand Up @@ -110,6 +122,14 @@ public int getType() {
return type;
}

/**
* @see IResourceChangeEvent#getBuilderName()
*/
@Override
public String getBuilderName() {
return builderName;
}

public void setDelta(IResourceDelta value) {
delta = value;
}
Expand All @@ -136,6 +156,12 @@ public String toDebugString() {
case PRE_REFRESH :
output.append("PRE_REFRESH"); //$NON-NLS-1$
break;
case PRE_PROJECT_BUILD :
output.append("PRE_PROJECT_BUILD"); //$NON-NLS-1$
break;
case POST_PROJECT_BUILD :
output.append("POST_PROJECT_BUILD"); //$NON-NLS-1$
break;
default :
output.append("?"); //$NON-NLS-1$
break;
Expand All @@ -157,6 +183,9 @@ public String toDebugString() {
break;
}
output.append("\nResource: " + (resource == null ? "null" : resource)); //$NON-NLS-1$ //$NON-NLS-2$
if (builderName != null) {
output.append("\nBuilder: " + builderName); //$NON-NLS-1$
}
output.append("\nDelta:" + (delta == null ? " null" : ((ResourceDelta) delta).toDeepDebugString())); //$NON-NLS-1$ //$NON-NLS-2$
return output.toString();
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*******************************************************************************
* Copyright (c) 2000, 2008 IBM Corporation and others.
* Copyright (c) 2000, 2026 IBM Corporation and others.
*
* This program and the accompanying materials
* are made available under the terms of the Eclipse Public License 2.0
Expand All @@ -15,6 +15,7 @@

import java.util.Objects;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.concurrent.atomic.AtomicIntegerArray;
import org.eclipse.core.resources.IResourceChangeListener;

/**
Expand Down Expand Up @@ -44,22 +45,18 @@ static final class ListenerEntry {

@Override
public String toString() {
StringBuilder sb = new StringBuilder();
sb.append("Listener [eventMask="); //$NON-NLS-1$
sb.append(eventMask);
sb.append(", "); //$NON-NLS-1$
sb.append(listener);
sb.append("]"); //$NON-NLS-1$
return sb.toString();
return "Listener [eventMask=" + eventMask + ", " + listener + "]"; //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$
}
}

private volatile int count1 = 0;
private volatile int count2 = 0;
private volatile int count4 = 0;
private volatile int count8 = 0;
private volatile int count16 = 0;
private volatile int count32 = 0;
/**
* Per-event-bit listener counts, indexed by
* {@code Integer.numberOfTrailingZeros(eventMask)}. An
* {@link AtomicIntegerArray} preserves the per-element volatile read
* semantics that {@link #hasListenerFor(int)} relies on outside the
* synchronized {@link #add}/{@link #remove}/{@link #clear} paths.
*/
private final AtomicIntegerArray bitCounts = new AtomicIntegerArray(Integer.SIZE);

/**
* The list of listeners.
Expand All @@ -79,43 +76,22 @@ public synchronized void add(IResourceChangeListener listener, int mask) {
remove(listener);
return;
}
ResourceChangeListenerList.ListenerEntry entry = new ResourceChangeListenerList.ListenerEntry(listener, mask);
ListenerEntry entry = new ListenerEntry(listener, mask);
final int oldSize = listeners.size();
// check for duplicates using identity
for (int i = 0; i < oldSize; ++i) {
ListenerEntry oldEntry = listeners.get(i);
if (oldEntry.listener == listener) {
removing(oldEntry.eventMask);
adding(mask);
adjust(oldEntry.eventMask, -1);
adjust(mask, +1);
listeners.set(i, entry);
return;
}
}
adding(mask);
adjust(mask, +1);
listeners.add(entry);
}

private void adding(int mask) {
if ((mask & 1) != 0) {
count1++;
}
if ((mask & 2) != 0) {
count2++;
}
if ((mask & 4) != 0) {
count4++;
}
if ((mask & 8) != 0) {
count8++;
}
if ((mask & 16) != 0) {
count16++;
}
if ((mask & 32) != 0) {
count32++;
}
}

/**
* Returns a copy of the registered listeners.
* @return the list of registered listeners that must not be modified
Expand All @@ -125,22 +101,11 @@ public ListenerEntry[] getListeners() {
}

public boolean hasListenerFor(int event) {
switch (event) {
case 1:
return count1 > 0;
case 2:
return count2 > 0;
case 4:
return count4 > 0;
case 8:
return count8 > 0;
case 16:
return count16 > 0;
case 32:
return count32 > 0;
default:
// event is expected to be a single bit (a power of two)
if (event <= 0 || Integer.bitCount(event) != 1) {
return false;
}
return bitCounts.get(Integer.numberOfTrailingZeros(event)) > 0;
}

/**
Expand All @@ -155,7 +120,7 @@ public synchronized void remove(IResourceChangeListener listener) {
for (int i = 0; i < oldSize; ++i) {
ListenerEntry oldEntry = listeners.get(i);
if (oldEntry.listener == listener) {
removing(oldEntry.eventMask);
adjust(oldEntry.eventMask, -1);
listeners.remove(i);
return;
}
Expand All @@ -164,44 +129,22 @@ public synchronized void remove(IResourceChangeListener listener) {

public synchronized void clear() {
listeners.clear();
count1 = 0;
count2 = 0;
count4 = 0;
count8 = 0;
count16 = 0;
count32 = 0;
for (int i = 0; i < bitCounts.length(); i++) {
bitCounts.set(i, 0);
}
}

private void removing(int mask) {
if ((mask & 1) != 0) {
count1--;
}
if ((mask & 2) != 0) {
count2--;
}
if ((mask & 4) != 0) {
count4--;
}
if ((mask & 8) != 0) {
count8--;
}
if ((mask & 16) != 0) {
count16--;
}
if ((mask & 32) != 0) {
count32--;
private void adjust(int mask, int delta) {
int remaining = mask;
while (remaining != 0) {
int bit = Integer.numberOfTrailingZeros(remaining);
bitCounts.addAndGet(bit, delta);
remaining &= remaining - 1;
}
}

@Override
public String toString() {
StringBuilder builder = new StringBuilder();
builder.append("ResourceChangeListenerList ["); //$NON-NLS-1$
if (listeners != null) {
builder.append("listeners="); //$NON-NLS-1$
builder.append(listeners.toString());
}
builder.append("]"); //$NON-NLS-1$
return builder.toString();
return "ResourceChangeListenerList [listeners=" + listeners + "]"; //$NON-NLS-1$ //$NON-NLS-2$
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -458,6 +458,15 @@ public void broadcastBuildEvent(Object source, int type, int buildTrigger) {
notificationManager.broadcastChanges(tree, event, false);
}

/**
* Broadcasts a per-builder build event. The {@code type} must be either
* {@link IResourceChangeEvent#PRE_PROJECT_BUILD} or
* {@link IResourceChangeEvent#POST_PROJECT_BUILD}.
*/
public void broadcastProjectBuildEvent(IProject project, String builderName, int type, int buildTrigger) {
notificationManager.broadcastProjectBuildEvent(project, builderName, type, buildTrigger);
}

/**
* Broadcasts an internal workspace lifecycle event to interested
* internal listeners.
Expand Down
Loading