| /******************************************************************************* |
| * Copyright (c) 2017 SSI Schaefer IT Solutions GmbH and others. |
| * All rights reserved. This program and the accompanying materials |
| * are made available under the terms of the Eclipse Public License v1.0 |
| * which accompanies this distribution, and is available at |
| * http://www.eclipse.org/legal/epl-v10.html |
| * |
| * Contributors: |
| * SSI Schaefer IT Solutions GmbH |
| *******************************************************************************/ |
| package org.eclipse.tea.core; |
| |
| import java.lang.annotation.Annotation; |
| import java.util.ArrayList; |
| import java.util.LinkedHashMap; |
| import java.util.List; |
| import java.util.Map; |
| |
| import javax.inject.Inject; |
| |
| import org.eclipse.core.resources.IWorkspace; |
| import org.eclipse.core.resources.ResourcesPlugin; |
| import org.eclipse.core.runtime.IProgressMonitor; |
| import org.eclipse.core.runtime.IStatus; |
| import org.eclipse.core.runtime.MultiStatus; |
| import org.eclipse.core.runtime.OperationCanceledException; |
| import org.eclipse.core.runtime.Status; |
| import org.eclipse.core.runtime.SubMonitor; |
| import org.eclipse.e4.core.contexts.ContextInjectionFactory; |
| import org.eclipse.e4.core.contexts.IEclipseContext; |
| import org.eclipse.e4.core.di.InjectionException; |
| import org.eclipse.e4.core.di.annotations.Execute; |
| import org.eclipse.e4.core.di.annotations.Optional; |
| import org.eclipse.e4.core.di.extensions.Service; |
| import org.eclipse.tea.core.annotations.TaskChainContextInit; |
| import org.eclipse.tea.core.annotations.TaskChainSuppressLifecycle; |
| import org.eclipse.tea.core.annotations.lifecycle.BeginTask; |
| import org.eclipse.tea.core.annotations.lifecycle.BeginTaskChain; |
| import org.eclipse.tea.core.annotations.lifecycle.CreateContext; |
| import org.eclipse.tea.core.annotations.lifecycle.DisposeContext; |
| import org.eclipse.tea.core.annotations.lifecycle.FinishTask; |
| import org.eclipse.tea.core.annotations.lifecycle.FinishTaskChain; |
| import org.eclipse.tea.core.internal.OutputRedirector; |
| import org.eclipse.tea.core.internal.TaskProgressEstimationService; |
| import org.eclipse.tea.core.internal.TaskProgressExtendedTracker; |
| import org.eclipse.tea.core.internal.TaskProgressTrackerImpl; |
| import org.eclipse.tea.core.internal.TaskingEngineActivator; |
| import org.eclipse.tea.core.internal.model.TaskingModel; |
| import org.eclipse.tea.core.services.TaskChain; |
| import org.eclipse.tea.core.services.TaskChain.TaskChainId; |
| import org.eclipse.tea.core.services.TaskProgressTracker; |
| import org.eclipse.tea.core.services.TaskProgressTracker.TaskProgressProvider; |
| import org.eclipse.tea.core.services.TaskingLifeCycleListener; |
| import org.eclipse.tea.core.services.TaskingLog; |
| |
| /** |
| * Controls the execution of a {@link TaskChain}. |
| */ |
| public class TaskExecutionContext { |
| |
| private final TaskChain chain; |
| private final IEclipseContext context; |
| |
| private final List<Object> tasks = new ArrayList<>(); |
| private final List<TaskingLifeCycleListener> listeners = new ArrayList<>(); |
| |
| @Inject |
| public TaskExecutionContext(IEclipseContext context, TaskChain chain, |
| @Service List<TaskingLifeCycleListener> listeners) throws Exception { |
| this.chain = chain; |
| this.context = context; |
| |
| // create fresh instances of all listeners - later use PROTOTYPE in DS |
| // 1.3 |
| for (TaskingLifeCycleListener listener : listeners) { |
| TaskingLifeCycleListener l = listener.getClass().newInstance(); |
| this.listeners.add(l); |
| |
| // register listener for internal direct access |
| context.set(l.getClass().getName(), l); |
| } |
| |
| // make ourself available to the task chain |
| context.set(TaskExecutionContext.class, this); |
| |
| // initialize this context |
| ContextInjectionFactory.invoke(chain, TaskChainContextInit.class, context); |
| |
| // only notify about context creation if there is something to do. an |
| // empty context will not have any effect on anything and will, in fact, |
| // not be executed at all by the engine. |
| if (!isEmpty()) { |
| notifyAll(CreateContext.class, context); |
| } |
| } |
| |
| /** |
| * @return the dependency injection context for the |
| * {@link TaskExecutionContext} |
| */ |
| public IEclipseContext getContext() { |
| return context; |
| } |
| |
| /** |
| * @return the number of retries that are allowed on failure |
| */ |
| public int getRetries() { |
| TaskChainId id = chain.getClass().getAnnotation(TaskChainId.class); |
| if (id == null) { |
| return 1; |
| } |
| return id.retries(); |
| } |
| |
| /** |
| * @return the underlying {@link TaskChain} which serves as a template for |
| * this execution context. Only to be used for informational |
| * purposes |
| */ |
| public TaskChain getUnderlyingChain() { |
| return chain; |
| } |
| |
| /** |
| * @return whether there are any tasks to execute in this context. |
| */ |
| public boolean isEmpty() { |
| return tasks.isEmpty(); |
| } |
| |
| /** |
| * @param o |
| * an arbitrary task that has a method annotated with the |
| * {@link Execute} annotation. Can be either the actual object or |
| * a the {@link Class} for the object to be created. It is also |
| * possible to add an instance or {@link Class} object of type |
| * {@link TaskChain}, which will inline expand all the tasks |
| * contained in the {@link TaskChain}. |
| */ |
| public void addTask(Object o) { |
| if (o instanceof TaskChain) { |
| ContextInjectionFactory.invoke(o, TaskChainContextInit.class, context); |
| } else if (o instanceof Class && TaskChain.class.isAssignableFrom((Class<?>) o)) { |
| Object tc = ContextInjectionFactory.make((Class<?>) o, context); |
| ContextInjectionFactory.invoke(tc, TaskChainContextInit.class, context); |
| } else { |
| tasks.add(o); |
| } |
| } |
| |
| /** |
| * Executes the tasks in the context. Exposes status and progress handling |
| * to tasks, manages life cycle events. |
| */ |
| @Execute |
| public void execute(TaskingLog log, @Optional @Service TaskProgressEstimationService progressService, |
| @Optional IProgressMonitor monitor) { |
| MultiStatus status = new MultiStatus(TaskingEngineActivator.PLUGIN_ID, IStatus.OK, |
| "Tasking Execution Context Status", null); |
| |
| context.set(IWorkspace.class, ResourcesPlugin.getWorkspace()); |
| context.set(MultiStatus.class, status); |
| context.activate(); |
| |
| Map<Object, IEclipseContext> taskContexts = new LinkedHashMap<>(); |
| int totalAmount = prepareTaskProgressTracking(log, progressService, taskContexts); |
| context.set(TaskingInjectionHelper.CTX_TASK_CONTEXTS, taskContexts); |
| |
| notifyAll(BeginTaskChain.class, context); |
| |
| SubMonitor rootMonitor = SubMonitor.convert(monitor, "Executing " + TaskingModel.getTaskChainName(chain), |
| totalAmount); |
| |
| // execute tasks |
| try { |
| for (Map.Entry<Object, IEclipseContext> ctx : taskContexts.entrySet()) { |
| Object task = ctx.getKey(); |
| IEclipseContext taskCtx = ctx.getValue(); |
| |
| Integer amount = (Integer) taskCtx.get(TaskingInjectionHelper.CTX_TASK_WORK_AMOUNT); |
| |
| // setup dedicated progress monitor, based on previous work |
| // amount calculation |
| SubMonitor taskMonitor = rootMonitor.split(amount); |
| taskMonitor.beginTask("Execute Task", amount); |
| taskMonitor.subTask(TaskingModel.getTaskName(task)); |
| |
| TaskProgressTracker tracker = new TaskProgressTrackerImpl(task, taskMonitor); |
| taskCtx.set(TaskProgressExtendedTracker.class, (TaskProgressExtendedTracker) tracker); |
| |
| notifyAll(BeginTask.class, taskCtx); |
| |
| // handle estimation request of tasks |
| // (TaskProgressEstimated) |
| String estimationId = progressService == null ? null : progressService.calculateId(task); |
| if (estimationId != null) { |
| // begin tracking with the real tracker |
| progressService.begin(estimationId, tracker); |
| |
| // forbid explicit updating of worked amount for tasks. |
| tracker = new TaskProgressTrackerImpl.RestrictedProgressTrackerImpl(tracker); |
| } |
| |
| // tracker is available in any case. if estimated it is |
| // restricted. |
| taskCtx.set(TaskProgressTracker.class, tracker); |
| |
| try { |
| // actually execute the task |
| executeSingleTask(log, task, taskCtx); |
| |
| } finally { |
| // in case the task set it's own status |
| IStatus taskStatus = taskCtx.get(IStatus.class); |
| status.add(taskStatus); |
| |
| // stop estimated progress for this task |
| if (estimationId != null && progressService != null) { |
| progressService.finish(estimationId, taskStatus); |
| } |
| |
| notifyAll(FinishTask.class, taskCtx); |
| } |
| |
| // handle ERROR and CANCEL status publishes when the task did |
| // not throw an exception |
| IStatus taskStatus = taskCtx.get(IStatus.class); |
| if (taskStatus.getSeverity() >= IStatus.ERROR) { |
| log.error("Task aborted with status " + taskStatus); |
| break; |
| } |
| |
| // update the root monitor to reflect the child amount of work |
| // that is now consumed after the task finished for good or bad. |
| totalAmount -= amount; |
| rootMonitor.setWorkRemaining(totalAmount); |
| } |
| } catch (Throwable t) { |
| status.add( |
| new Status(IStatus.ERROR, TaskingEngineActivator.PLUGIN_ID, "Failed to texecute " + toString(), t)); |
| throw t; |
| } finally { |
| notifyAll(FinishTaskChain.class, context); |
| |
| // need a second step to avoid races with listener list. |
| notifyAll(DisposeContext.class, context); |
| context.deactivate(); |
| } |
| } |
| |
| /** |
| * @param log |
| * the log to use if required |
| * @param task |
| * the task to execute |
| * @param taskCtx |
| * the {@link IEclipseContext} to use for dependency injection. |
| */ |
| private static void executeSingleTask(TaskingLog log, Object task, IEclipseContext taskCtx) { |
| OutputRedirector redir = new OutputRedirector(task, log); |
| |
| taskCtx.set(IStatus.class, |
| new Status(IStatus.OK, TaskingEngineActivator.PLUGIN_ID, "Task: " + TaskingModel.getTaskName(task))); |
| try { |
| // possibly redirect system.out and system.err to the log |
| redir.begin(); |
| |
| // make task's context the active leaf |
| taskCtx.activate(); |
| |
| // and run the task |
| Object result = ContextInjectionFactory.invoke(task, Execute.class, taskCtx); |
| |
| // check if a status was returned |
| if (result instanceof IStatus) { |
| taskCtx.set(IStatus.class, (IStatus) result); |
| } |
| } catch (Throwable t) { |
| if (t instanceof InjectionException && t.getCause() instanceof OperationCanceledException) { |
| OperationCanceledException oce = (OperationCanceledException) t.getCause(); |
| taskCtx.set(IStatus.class, new Status(IStatus.CANCEL, TaskingEngineActivator.PLUGIN_ID, |
| "Cancelled: " + TaskingModel.getTaskName(task), oce)); |
| } else { |
| taskCtx.set(IStatus.class, new Status(IStatus.ERROR, TaskingEngineActivator.PLUGIN_ID, |
| "Fatal failure while executing " + TaskingModel.getTaskName(task), t)); |
| } |
| } finally { |
| // reset redirection |
| redir.finish(); |
| |
| // notify listeners and deactivate the context last. |
| taskCtx.deactivate(); |
| } |
| } |
| |
| /** |
| * Prepares progress tracking for tasks by: |
| * <ol> |
| * <li>creating task instances where required, so that all tasks are |
| * available |
| * <li>call |
| * {@link #prepareSingleTaskProgressTracking(TaskingLog, TaskProgressEstimationService, Map, Object)} |
| * for each task |
| * <li>summing up the amount of work for each task and returning the total |
| * amount of work. |
| * </ol> |
| * |
| * @param log |
| * the log to use if required |
| * @param progressService |
| * the {@link TaskProgressEstimationService} if available, |
| * otherwise <code>null</code> |
| * @param taskContexts |
| * a map (probably want to use {@link LinkedHashMap}) that will |
| * be filled with task-to-context mapping |
| * @return the total amount of work for all tasks |
| */ |
| private int prepareTaskProgressTracking(TaskingLog log, TaskProgressEstimationService progressService, |
| Map<Object, IEclipseContext> taskContexts) { |
| List<Object> instances = prepareTaskInstances(); |
| context.set(TaskingInjectionHelper.CTX_PREPARED_TASKS, instances); |
| |
| // gather progress information in two stages. |
| int totalAmount = 0; |
| for (Object o : instances) { |
| totalAmount += prepareSingleTaskProgressTracking(log, progressService, taskContexts, o); |
| } |
| return totalAmount; |
| } |
| |
| /** |
| * @param log |
| * the log to use if required |
| * @param progressService |
| * the {@link TaskProgressEstimationService} if available, |
| * <code>null</code> otherwise. |
| * @param taskContexts |
| * a map (probably want to use {@link LinkedHashMap}) that will |
| * be filled with task-to-context mapping |
| * @param o |
| * the task to prepare tracking for |
| * @return the amount of work that this single task will allocate |
| */ |
| private int prepareSingleTaskProgressTracking(TaskingLog log, TaskProgressEstimationService progressService, |
| Map<Object, IEclipseContext> taskContexts, Object o) { |
| Integer amount = getTaskWorkAmount(log, o, progressService); |
| |
| IEclipseContext taskContext = context.createChild(o.getClass().getName()); |
| taskContext.set(TaskingInjectionHelper.CTX_TASK_WORK_AMOUNT, amount); |
| taskContext.set(TaskingInjectionHelper.CTX_TASK, o); |
| |
| taskContexts.put(o, taskContext); |
| return amount; |
| } |
| |
| /** |
| * Determines the work amount of a single task. This may either be |
| * <ul> |
| * <li>an explicit value given by a {@link TaskProgressProvider} method |
| * <li>an estimation provided by the given |
| * {@link TaskProgressEstimationService}. |
| * </ul> |
| * |
| * @param log |
| * the log to use if required |
| * @param o |
| * the task to get work amount for |
| * @param service |
| * the {@link TaskProgressEstimationService} if available, |
| * <code>null</code> otherwise |
| * @return the absolute amount of work for this single task |
| */ |
| private Integer getTaskWorkAmount(TaskingLog log, Object o, TaskProgressEstimationService service) { |
| String id = service == null ? null : service.calculateId(o); |
| if (id != null) { |
| return service.getEstimatedTicks(id); |
| } else { |
| // this code path will only hit if there is a TaskProgressProvider |
| // method found by the service |
| try { |
| return (Integer) ContextInjectionFactory.invoke(o, TaskProgressProvider.class, context, |
| Integer.valueOf(1)); |
| } catch (Exception e) { |
| log.debug("Failed to determine amount of work for " + TaskingModel.getTaskName(o), e); |
| return 1; |
| } |
| } |
| } |
| |
| /** |
| * Prepares tasks by creating instances for tasks that have been added as |
| * {@link Class}. |
| * |
| * @return a list of task instances |
| */ |
| private List<Object> prepareTaskInstances() { |
| List<Object> result = new ArrayList<>(); |
| |
| for (Object o : tasks) { |
| if (o instanceof Class) { |
| o = ContextInjectionFactory.make((Class<?>) o, context); |
| } |
| result.add(o); |
| } |
| |
| return result; |
| } |
| |
| private boolean isSuppressed(Class<? extends TaskChain> tcClass) { |
| TaskChainSuppressLifecycle ann = tcClass.getAnnotation(TaskChainSuppressLifecycle.class); |
| if (ann == null || ann.value() == false) { |
| return false; |
| } |
| |
| return true; |
| } |
| |
| /** |
| * Notify all registered {@link TaskingLifeCycleListener} about a given |
| * event. |
| * |
| * @param event |
| * the event |
| * @param ctx |
| * the context used for dependency injection on the listeners. |
| */ |
| private void notifyAll(Class<? extends Annotation> event, IEclipseContext ctx) { |
| if (isSuppressed(chain.getClass())) { |
| return; |
| } |
| |
| for (TaskingLifeCycleListener l : listeners) { |
| ContextInjectionFactory.invoke(l, event, ctx, null); |
| } |
| } |
| |
| @Override |
| public String toString() { |
| return "ExecutionContext[" + chain.getClass().getName() + "]"; |
| } |
| |
| } |