blob: 19c12b05984917d347df0447983f5ebbf2cc8cf2 [file] [log] [blame]
/*******************************************************************************
* Copyright (c) 2003, 2020 IBM Corporation and others.
*
* This program and the accompanying materials
* are made available under the terms of the Eclipse Public License 2.0
* which accompanies this distribution, and is available at
* https://www.eclipse.org/legal/epl-2.0/
*
* SPDX-License-Identifier: EPL-2.0
*
* Contributors:
* IBM Corporation - initial API and implementation
* Teddy Walker <teddy.walker@googlemail.com>
* - Fix for Bug 151204 [Progress] Blocked status of jobs are not applied/reported
* Lars Vogel <Lars.Vogel@gmail.com> - Bug 422040
* Philipp Bumann <bumannp@gmail.com> - Bug 477602
*******************************************************************************/
package org.eclipse.e4.ui.progress.internal;
import java.io.IOException;
import java.io.InputStream;
import java.net.URL;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.Map;
import java.util.Map.Entry;
import javax.annotation.PostConstruct;
import javax.inject.Inject;
import org.eclipse.core.runtime.IProgressMonitor;
import org.eclipse.core.runtime.IProgressMonitorWithBlocking;
import org.eclipse.core.runtime.IStatus;
import org.eclipse.core.runtime.ListenerList;
import org.eclipse.core.runtime.NullProgressMonitor;
import org.eclipse.core.runtime.QualifiedName;
import org.eclipse.core.runtime.Status;
import org.eclipse.core.runtime.jobs.IJobChangeEvent;
import org.eclipse.core.runtime.jobs.IJobChangeListener;
import org.eclipse.core.runtime.jobs.Job;
import org.eclipse.core.runtime.jobs.JobChangeAdapter;
import org.eclipse.core.runtime.jobs.ProgressProvider;
import org.eclipse.e4.core.di.annotations.Optional;
import org.eclipse.e4.ui.progress.IProgressConstants;
import org.eclipse.e4.ui.progress.IProgressService;
import org.eclipse.e4.ui.progress.UIJob;
import org.eclipse.e4.ui.progress.internal.legacy.EventLoopProgressMonitor;
import org.eclipse.e4.ui.progress.internal.legacy.PlatformUI;
import org.eclipse.e4.ui.progress.internal.legacy.Policy;
import org.eclipse.jface.dialogs.Dialog;
import org.eclipse.swt.graphics.Image;
import org.eclipse.swt.graphics.ImageData;
import org.eclipse.swt.graphics.ImageLoader;
import org.eclipse.swt.widgets.Display;
/**
* JobProgressManager provides the progress monitor to the job manager and
* informs any ProgressContentProviders of changes.
*/
public class ProgressManager extends ProgressProvider {
/**
* A property to determine if the job was run in the dialog. Kept for backwards
* compatibility.
*
* @deprecated use IProgressConstants#PROPERTY_IN_DIALOG instead
*/
@Deprecated
public static final QualifiedName PROPERTY_IN_DIALOG = IProgressConstants.PROPERTY_IN_DIALOG;
private static final String ERROR_JOB = "errorstate.png"; //$NON-NLS-1$
static final String ERROR_JOB_KEY = "ERROR_JOB"; //$NON-NLS-1$
private static ProgressManager singleton;
final private Map<Job, JobInfo> jobs = Collections
.synchronizedMap(new HashMap<Job, JobInfo>());
final Map<Job, JobMonitor> runnableMonitors = new HashMap<>();
final private Map<Object, Collection<IJobBusyListener>> familyListeners = Collections
.synchronizedMap(new HashMap<>());
// list of IJobProgressManagerListener
private ListenerList<IJobProgressManagerListener> listeners = new ListenerList<>();
IJobChangeListener changeListener;
static final String PROGRESS_VIEW_NAME = "org.eclipse.e4.ui.progress.ProgressView"; //$NON-NLS-1$
static final String PROGRESS_FOLDER = "progress/"; //$NON-NLS-1$
private static final String SLEEPING_JOB = "sleeping.png"; //$NON-NLS-1$
private static final String WAITING_JOB = "waiting.png"; //$NON-NLS-1$
private static final String BLOCKED_JOB = "lockedstate.png"; //$NON-NLS-1$
/**
* The key for the sleeping job icon.
*/
public static final String SLEEPING_JOB_KEY = "SLEEPING_JOB"; //$NON-NLS-1$
/**
* The key for the waiting job icon.
*/
public static final String WAITING_JOB_KEY = "WAITING_JOB"; //$NON-NLS-1$
/**
* The key for the locked job icon.
*/
public static final String BLOCKED_JOB_KEY = "LOCKED_JOB"; //$NON-NLS-1$
@Inject
@Optional
IProgressService progressService;
@Inject
JobInfoFactory jobInfoFactory;
@Inject
FinishedJobs finishedJobs;
/**
* Shutdown the singleton if there is one.
*/
public static void shutdownProgressManager() {
if (singleton == null) {
return;
}
singleton.shutdown();
}
@PostConstruct
protected void init(WorkbenchDialogBlockedHandler dialogBlockedHandler) {
Dialog.setBlockedHandler(dialogBlockedHandler);
setUpImages();
changeListener = createChangeListener();
Job.getJobManager().setProgressProvider(this);
Job.getJobManager().addJobChangeListener(this.changeListener);
addListener(finishedJobs.listener);
}
private void setUpImages() {
ImageTools imageTools = ImageTools.getInstance();
imageTools.putIntoRegistry(SLEEPING_JOB_KEY, PROGRESS_FOLDER
+ SLEEPING_JOB);
imageTools.putIntoRegistry(WAITING_JOB_KEY, PROGRESS_FOLDER
+ WAITING_JOB);
imageTools.putIntoRegistry(BLOCKED_JOB_KEY, PROGRESS_FOLDER
+ BLOCKED_JOB);
imageTools.putIntoRegistry(ERROR_JOB_KEY, PROGRESS_FOLDER + ERROR_JOB);
}
@Override
public IProgressMonitor createMonitor(Job job) {
return progressFor(job);
}
/**
* Return a monitor for the job. Check if we cached a monitor for this job
* previously for a long operation timeout check.
*
* @param job
* @return IProgressMonitor
*/
public JobMonitor progressFor(Job job) {
synchronized (runnableMonitors) {
JobMonitor monitor = runnableMonitors.get(job);
if (monitor == null) {
monitor = new JobMonitor(job);
runnableMonitors.put(job, monitor);
}
return monitor;
}
}
@Override
public IProgressMonitor createProgressGroup() {
return new GroupInfo(this, finishedJobs);
}
@Override
public IProgressMonitor createMonitor(Job job, IProgressMonitor group,
int ticks) {
JobMonitor monitor = progressFor(job);
if (group instanceof GroupInfo) {
GroupInfo groupInfo = (GroupInfo) group;
JobInfo jobInfo = getJobInfo(job);
jobInfo.setGroupInfo(groupInfo);
jobInfo.setTicks(ticks);
groupInfo.addJobInfo(jobInfo);
}
return monitor;
}
/**
* Get the JobInfo for the job. If it does not exist create it.
*
* @param job
* @return JobInfo
*/
JobInfo getJobInfo(Job job) {
JobInfo info = internalGetJobInfo(job);
if (info == null) {
info = jobInfoFactory.getJobInfo(job);
jobs.put(job, info);
}
return info;
}
/**
* Return an existing job info for the given Job or <code>null</code> if
* there isn't one.
*
* @param job
* @return JobInfo
*/
JobInfo internalGetJobInfo(Job job) {
return jobs.get(job);
}
/**
* The JobMonitor is the inner class that handles the IProgressMonitor
* integration with the ProgressMonitor.
*/
class JobMonitor implements IProgressMonitorWithBlocking {
Job job;
String currentTaskName;
IProgressMonitorWithBlocking listener;
/**
* Create a monitor on the supplied job.
*
* @param newJob
*/
JobMonitor(Job newJob) {
job = newJob;
}
/**
* Add monitor as another monitor that
*
* @param monitor
*/
void addProgressListener(IProgressMonitorWithBlocking monitor) {
listener = monitor;
JobInfo info = getJobInfo(job);
TaskInfo currentTask = info.getTaskInfo();
if (currentTask != null) {
listener.beginTask(currentTaskName, currentTask.totalWork);
listener.internalWorked(currentTask.preWork);
}
}
@Override
public void beginTask(String taskName, int totalWork) {
JobInfo info = getJobInfo(job);
info.beginTask(taskName, totalWork);
refreshJobInfo(info);
currentTaskName = taskName;
if (listener != null) {
listener.beginTask(taskName, totalWork);
}
}
@Override
public void done() {
synchronized (runnableMonitors) {
JobInfo info = getJobInfo(job);
info.clearTaskInfo();
info.clearChildren();
runnableMonitors.remove(job);
}
if (listener != null) {
listener.done();
}
}
@Override
public void internalWorked(double work) {
JobInfo info = getJobInfo(job);
if (info.hasTaskInfo()) {
info.addWork(work);
refreshJobInfo(info);
}
if (listener != null) {
listener.internalWorked(work);
}
}
@Override
public boolean isCanceled() {
// Use the internal get so we don't create a Job Info for
// a job that is not running (see bug 149857)
JobInfo info = internalGetJobInfo(job);
if (info == null)
return false;
return info.isCanceled();
}
@Override
public void setCanceled(boolean value) {
JobInfo info = getJobInfo(job);
// Don't bother cancelling twice
if (value && !info.isCanceled()) {
info.cancel();
// Only inform the first time
if (listener != null) {
listener.setCanceled(value);
}
}
}
@Override
public void setTaskName(String taskName) {
JobInfo info = getJobInfo(job);
if (info.hasTaskInfo()) {
info.setTaskName(taskName);
} else {
beginTask(taskName, 100);
return;
}
info.clearChildren();
refreshJobInfo(info);
currentTaskName = taskName;
if (listener != null) {
listener.setTaskName(taskName);
}
}
@Override
public void subTask(String name) {
if (name == null) {
return;
}
JobInfo info = getJobInfo(job);
info.clearChildren();
info.addSubTask(name);
refreshJobInfo(info);
if (listener != null) {
listener.subTask(name);
}
}
@Override
public void worked(int work) {
internalWorked(work);
}
@Override
public void clearBlocked() {
JobInfo info = getJobInfo(job);
info.setBlockedStatus(null);
refreshJobInfo(info);
if (listener != null) {
listener.clearBlocked();
}
}
@Override
public void setBlocked(IStatus reason) {
JobInfo info = getJobInfo(job);
info.setBlockedStatus(reason);
refreshJobInfo(info);
if (listener != null) {
listener.setBlocked(reason);
}
}
}
/**
* Create and return the IJobChangeListener registered with the Job manager.
*
* @return the created IJobChangeListener
*/
private IJobChangeListener createChangeListener() {
return new JobChangeAdapter() {
@Override
public void aboutToRun(IJobChangeEvent event) {
JobInfo info = getJobInfo(event.getJob());
refreshJobInfo(info);
Iterator<IJobBusyListener> startListeners = busyListenersForJob(event.getJob())
.iterator();
while (startListeners.hasNext()) {
IJobBusyListener next = startListeners.next();
next.incrementBusy(event.getJob());
}
}
@Override
public void done(IJobChangeEvent event) {
if (!PlatformUI.isWorkbenchRunning()) {
return;
}
Iterator<IJobBusyListener> startListeners = busyListenersForJob(event.getJob())
.iterator();
while (startListeners.hasNext()) {
IJobBusyListener next = startListeners
.next();
next.decrementBusy(event.getJob());
}
removeJob(event.getJob());
//TODO E4
// if (event.getResult() != null
// && event.getResult().getSeverity() == IStatus.ERROR
// && event
// .getJob()
// .getProperty(
// IProgressConstants.NO_IMMEDIATE_ERROR_PROMPT_PROPERTY) != Boolean.TRUE) {
//
// ExternalServices.getStatusReporter().report(event.getResult(), StatusReporter.SHOW, new Object[0]);
// }
}
@Override
public void scheduled(IJobChangeEvent event) {
updateFor(event);
if (event.getJob().isUser()) {
boolean noDialog = shouldRunInBackground();
if (!noDialog) {
final IJobChangeEvent finalEvent = event;
Job showJob = new UIJob(
ProgressMessages.ProgressManager_showInDialogName) {
@Override
public IStatus runInUIThread(
IProgressMonitor monitor) {
progressService.showInDialog(null, finalEvent.getJob());
return Status.OK_STATUS;
}
};
showJob.setSystem(true);
showJob.schedule();
return;
}
}
}
/**
* Update the listeners for the receiver for the event.
*
* @param event
*/
private void updateFor(IJobChangeEvent event) {
if (isInfrastructureJob(event.getJob())) {
return;
}
if (jobs.containsKey(event.getJob())) {
refreshJobInfo(getJobInfo(event.getJob()));
} else {
addJobInfo(jobInfoFactory.getJobInfo(event.getJob()));
}
}
@Override
public void awake(IJobChangeEvent event) {
updateFor(event);
}
@Override
public void sleeping(IJobChangeEvent event) {
if (jobs.containsKey(event.getJob()))// Are we showing this?
sleepJobInfo(getJobInfo(event.getJob()));
}
};
}
/**
* The job in JobInfo is now sleeping. Refresh it if we are showing it, remove
* it if not.
*
* @param info the job going to sleep
*/
protected void sleepJobInfo(JobInfo info) {
if (isInfrastructureJob(info.getJob()))
return;
GroupInfo group = info.getGroupInfo();
if (group != null) {
sleepGroup(group,info);
}
for (IJobProgressManagerListener listener : listeners) {
// Is this one the user never sees?
if (isNeverDisplaying(info.getJob(), listener.showsDebug()))
continue;
if (listener.showsDebug())
listener.refreshJobInfo(info);
else
listener.removeJob(info);
}
}
@Override
public IProgressMonitor getDefaultMonitor() {
// only need a default monitor for operations the UI thread
// and only if there is a display
Display display;
if (PlatformUI.isWorkbenchRunning() && !PlatformUI.isWorkbenchStarting()) {
display = getDisplay();
if (!display.isDisposed() && (display.getThread() == Thread.currentThread())) {
return new EventLoopProgressMonitor(new NullProgressMonitor());
}
}
return super.getDefaultMonitor();
}
/**
* Refresh the group when info is sleeping.
* @param group
*/
private void sleepGroup(GroupInfo group, JobInfo info) {
for (IJobProgressManagerListener listener : listeners) {
if (isNeverDisplaying(info.getJob(), listener.showsDebug()))
continue;
if (listener.showsDebug() || group.isActive())
listener.refreshGroup(group);
else
listener.removeGroup(group);
}
}
/**
* Add an IJobProgressManagerListener to listen to the changes.
*
* @param listener
*/
void addListener(IJobProgressManagerListener listener) {
listeners.add(listener);
}
/**
* Remove the supplied IJobProgressManagerListener from the list of
* listeners.
*
* @param listener
*/
void removeListener(IJobProgressManagerListener listener) {
listeners.remove(listener);
}
/**
* Refresh the IJobProgressManagerListeners as a result of a change in info.
*
* @param info the updated job info
*/
public void refreshJobInfo(JobInfo info) {
GroupInfo group = info.getGroupInfo();
if (group != null) {
refreshGroup(group);
}
for (IJobProgressManagerListener listener : listeners) {
if (!isNeverDisplaying(info.getJob(), listener.showsDebug())) {
listener.refreshJobInfo(info);
}
}
}
/**
* Refresh the IJobProgressManagerListeners as a result of a change in info.
*
* @param info the updated job group
*/
public void refreshGroup(GroupInfo info) {
for (IJobProgressManagerListener element : listeners) {
element.refreshGroup(info);
}
}
/**
* Refresh all the IJobProgressManagerListener as a result of a change in
* the whole model.
*/
public void refreshAll() {
pruneStaleJobs();
for (IJobProgressManagerListener element : listeners) {
element.refreshAll();
}
}
/**
* Refresh the content providers as a result of a deletion of job.
*
* @param job the job to remove information about
* @return the removed job info
*/
public JobInfo removeJob(Job job) {
JobInfo info;
synchronized (runnableMonitors) {
info = getJobInfo(job);
jobs.remove(job);
runnableMonitors.remove(job);
}
for (IJobProgressManagerListener listener : listeners) {
if (!isNeverDisplaying(info.getJob(), listener.showsDebug())) {
listener.removeJob(info);
}
}
return info;
}
/**
* Refresh the content providers as a result of a deletion of info.
*
* @param info JobInfo
* @deprecated use the more thread safe {@link #removeJob(Job)} instead. See bug
* 558655.
*/
@Deprecated
public void removeJobInfo(JobInfo info) {
removeJob(info.getJob());
}
/**
* Remove the group from the roots and inform the listeners.
*
* @param group
* GroupInfo
*/
public void removeGroup(GroupInfo group) {
for (IJobProgressManagerListener element : listeners) {
element.removeGroup(group);
}
}
/**
* Refresh the content providers as a result of an addition of info.
*
* @param info the added job info
*/
public void addJobInfo(JobInfo info) {
GroupInfo group = info.getGroupInfo();
if (group != null) {
refreshGroup(group);
}
jobs.put(info.getJob(), info);
for (IJobProgressManagerListener listener : listeners) {
if (!isCurrentDisplaying(info.getJob(), listener.showsDebug())) {
listener.addJob(info);
}
}
}
/**
* Return whether or not this job is currently displayable.
*
* @param job
* @param debug
* If the listener is in debug mode.
* @return boolean <code>true</code> if the job is not displayed.
*/
boolean isCurrentDisplaying(Job job, boolean debug) {
return isNeverDisplaying(job, debug) || job.getState() == Job.SLEEPING;
}
/**
* Return whether or not we even display this job with debug mode set to
* debug.
*
* @param job
* @param debug
* @return boolean
*/
boolean isNeverDisplaying(Job job, boolean debug) {
if (isInfrastructureJob(job)) {
return true;
}
if (debug)
return false;
return job.isSystem();
}
/**
* Return whether or not this job is an infrastructure job.
*
* @param job
* @return boolean <code>true</code> if it is never displayed.
*/
private boolean isInfrastructureJob(Job job) {
if (Policy.DEBUG_SHOW_ALL_JOBS)
return false;
return job.getProperty(ProgressManagerUtil.INFRASTRUCTURE_PROPERTY) != null;
}
/**
* Return the current job infos filtered on debug mode.
*
* @param debug if the listener is in debug mode
* @return JobInfo[]
*/
public JobInfo[] getJobInfos(boolean debug) {
synchronized (jobs) {
Collection<JobInfo> result = new ArrayList<>();
for (Entry<Job, JobInfo> entry : jobs.entrySet()) {
if (!isCurrentDisplaying(entry.getKey(), debug)) {
result.add(entry.getValue());
}
}
JobInfo[] infos = new JobInfo[result.size()];
result.toArray(infos);
return infos;
}
}
/**
* Return the current root elements filtered on the debug mode.
*
* @param debug if the listener is in debug mode
* @return JobTreeElement[]
*/
public JobTreeElement[] getRootElements(boolean debug) {
synchronized (jobs) {
Collection<JobTreeElement> result = new HashSet<>();
for (Entry<Job, JobInfo> entry : jobs.entrySet()) {
if (!isCurrentDisplaying(entry.getKey(), debug)) {
JobInfo jobInfo = entry.getValue();
GroupInfo group = jobInfo.getGroupInfo();
if (group == null) {
result.add(jobInfo);
} else {
result.add(group);
}
}
}
JobTreeElement[] infos = new JobTreeElement[result.size()];
result.toArray(infos);
return infos;
}
}
/**
* Return whether or not there are any jobs being displayed.
*
* @return boolean
*/
public boolean hasJobInfos() {
synchronized (jobs) {
return !jobs.isEmpty();
}
}
/**
* Returns the image descriptor with the given relative path.
*
* @param source
* @return Image
*/
Image getImage(ImageData source) {
ImageData mask = source.getTransparencyMask();
return new Image(null, source, mask);
}
/**
* Returns the image descriptor with the given relative path.
*
* @param fileSystemPath
* The URL for the file system to the image.
* @param loader -
* the loader used to get this data
* @return ImageData[]
*/
ImageData[] getImageData(URL fileSystemPath, ImageLoader loader) {
try (InputStream stream = fileSystemPath.openStream()) {
return loader.load(stream);
} catch (IOException exception) {
ProgressManagerUtil.logException(exception);
return null;
}
}
/**
* Shutdown the receiver.
*/
private void shutdown() {
listeners.clear();
Job.getJobManager().setProgressProvider(null);
Job.getJobManager().removeJobChangeListener(this.changeListener);
}
/**
* Add the listener to the family.
*
* @param family
* @param listener
*/
void addListenerToFamily(Object family, IJobBusyListener listener) {
synchronized (familyListeners) {
Collection<IJobBusyListener> currentListeners = familyListeners.get(family);
if (currentListeners == null) {
currentListeners = new HashSet<>();
familyListeners.put(family, currentListeners);
}
currentListeners.add(listener);
}
}
/**
* Remove the listener from all families.
*
* @param listener
*/
void removeListener(IJobBusyListener listener) {
synchronized (familyListeners) {
Iterator<Collection<IJobBusyListener>> familyListeners = this.familyListeners.values().iterator();
while (familyListeners.hasNext()) {
Collection<IJobBusyListener> currentListeners = familyListeners.next();
currentListeners.remove(listener);
// Remove any empty listeners
if (currentListeners.isEmpty()) {
familyListeners.remove();
}
}
}
}
/**
* Return the listeners for the job.
*
* @param job
* @return Collection of IJobBusyListener
*/
private Collection<IJobBusyListener> busyListenersForJob(Job job) {
if (job.isSystem()) {
return Collections.emptyList();
}
synchronized (familyListeners) {
if (familyListeners.isEmpty()) {
return Collections.emptyList();
}
Collection<IJobBusyListener> returnValue = new HashSet<>();
for (Entry<Object, Collection<IJobBusyListener>> entry : familyListeners.entrySet()) {
if (job.belongsTo(entry.getKey())) {
Collection<IJobBusyListener> currentListeners = entry.getValue();
returnValue.addAll(currentListeners);
}
}
return returnValue;
}
}
/**
* Check to see if there are any stale jobs we have not cleared out.
*
* @return <code>true</code> if anything was pruned
*/
private boolean pruneStaleJobs() {
Object[] jobsToCheck = jobs.keySet().toArray();
boolean pruned = false;
for (Object element : jobsToCheck) {
Job job = (Job) element;
if (checkForStaleness(job)) {
pruned = true;
}
}
return pruned;
}
/**
* Check the if the job should be removed from the list as it may be stale.
*
* @param job
* @return boolean
*/
boolean checkForStaleness(Job job) {
if (job.getState() == Job.NONE) {
removeJob(job);
return true;
}
return false;
}
/**
* Return whether or not dialogs should be run in the background
*
* @return <code>true</code> if the dialog should not be shown.
*/
protected boolean shouldRunInBackground() {
return Preferences.getBoolean(IProgressConstants.RUN_IN_BACKGROUND);
}
protected Display getDisplay() {
return Services.getInstance().getDisplay();
}
}