/******************************************************************************* * Copyright (c) 2012-2015 Codenvy, S.A. * 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: * Codenvy, S.A. - initial API and implementation *******************************************************************************/ package org.eclipse.che.api.builder.internal; import org.eclipse.che.api.builder.BuilderException; import org.eclipse.che.api.builder.dto.BaseBuilderRequest; import org.eclipse.che.api.builder.dto.BuildRequest; import org.eclipse.che.api.builder.dto.BuilderEnvironment; import org.eclipse.che.api.builder.dto.BuilderMetric; import org.eclipse.che.api.builder.dto.DependencyRequest; import org.eclipse.che.api.builder.internal.BuilderEvent.EventType; import org.eclipse.che.api.core.ApiException; import org.eclipse.che.api.core.NotFoundException; import org.eclipse.che.api.core.notification.EventService; import org.eclipse.che.api.core.notification.EventSubscriber; import org.eclipse.che.api.core.util.Cancellable; import org.eclipse.che.api.core.util.CancellableProcessWrapper; import org.eclipse.che.api.core.util.CommandLine; import org.eclipse.che.api.core.util.ProcessUtil; import org.eclipse.che.api.core.util.StreamPump; import org.eclipse.che.api.core.util.Watchdog; import org.eclipse.che.commons.lang.IoUtil; import org.eclipse.che.dto.server.DtoFactory; import com.google.common.util.concurrent.ThreadFactoryBuilder; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import javax.annotation.PostConstruct; import javax.annotation.PreDestroy; import java.io.IOException; import java.util.Collections; import java.util.Iterator; import java.util.LinkedHashSet; import java.util.LinkedList; import java.util.List; import java.util.Map; import java.util.Set; import java.util.concurrent.Callable; import java.util.concurrent.CancellationException; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentMap; import java.util.concurrent.CopyOnWriteArraySet; import java.util.concurrent.ExecutionException; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.FutureTask; import java.util.concurrent.LinkedBlockingQueue; import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.ThreadPoolExecutor; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicLong; /** * Super-class for all implementation of Builder. * * @author andrew00x */ public abstract class Builder { private static final Logger LOG = LoggerFactory.getLogger(Builder.class); private static final AtomicLong buildIdSequence = new AtomicLong(1); private final ConcurrentMap<Long, FutureBuildTask> tasks; private final java.io.File rootDirectory; private final Set<BuildListener> buildListeners; private final long keepResultTimeMillis; private final EventService eventService; private final int queueSize; private final int numberOfWorkers; private final AtomicBoolean started; private ThreadPoolExecutor executor; private ScheduledExecutorService scheduler; private java.io.File repository; private java.io.File builds; private SourcesManagerImpl sourcesManager; public Builder(java.io.File rootDirectory, int numberOfWorkers, int queueSize, int keepResultTime, EventService eventService) { this.rootDirectory = rootDirectory; this.numberOfWorkers = numberOfWorkers; this.queueSize = queueSize; this.keepResultTimeMillis = TimeUnit.SECONDS.toMillis(keepResultTime); this.eventService = eventService; buildListeners = new CopyOnWriteArraySet<>(); tasks = new ConcurrentHashMap<>(); started = new AtomicBoolean(false); } /** * Returns the name of the builder. All registered builders should have unique name. * * @return the name */ public abstract String getName(); /** * Returns the description of the builder. Description should help client to recognize correct type of builder for an application. * * @return the description of builder */ public abstract String getDescription(); /** * Gets environments that are supported by the builder. Each environment presupposes an existing some embedded pre-configured * environment for build, e.g. different versions of JVM. By default this method returns empty map that means usage single environment * for all builds. */ public Map<String, BuilderEnvironment> getEnvironments() { return Collections.emptyMap(); } /** * Gets result of FutureBuildTask. Getting result is implementation specific and mostly depends to build system, e.g. maven usually * stores build result in directory 'target' but it is not rule for ant. Regular users are not expected to use this method directly. * They should always use method {@link BuildTask#getResult()} instead. * * @param task * task * @param successful * reports whether build process terminated normally or not. * Note: {@code true} is not indicated successful build but only normal process termination. Build itself may be unsuccessful * because to compilation error, failed tests, etc. * @return BuildResult * @throws BuilderException * if an error occurs when try to get result * @see BuildTask#getResult() */ protected abstract BuildResult getTaskResult(FutureBuildTask task, boolean successful) throws BuilderException; protected abstract CommandLine createCommandLine(BuilderConfiguration config) throws BuilderException; /** Initialize Builder. Sub-classes should invoke {@code super.start} at the begin of this method. */ @PostConstruct public void start() { if (started.compareAndSet(false, true)) { repository = new java.io.File(rootDirectory, getName()); if (!(repository.exists() || repository.mkdirs())) { throw new IllegalStateException(String.format("Unable create directory %s", repository.getAbsolutePath())); } final java.io.File sources = new java.io.File(repository, "sources"); if (!(sources.exists() || sources.mkdirs())) { throw new IllegalStateException(String.format("Unable create directory %s", sources.getAbsolutePath())); } builds = new java.io.File(repository, "builds"); if (!(builds.exists() || builds.mkdirs())) { throw new IllegalStateException(String.format("Unable create directory %s", builds.getAbsolutePath())); } // TODO: use single instance of SourceManager sourcesManager = new SourcesManagerImpl(sources); sourcesManager.start(); // TODO: guice must do this executor = new MyThreadPoolExecutor(numberOfWorkers <= 0 ? Runtime.getRuntime().availableProcessors() : numberOfWorkers, queueSize); scheduler = Executors.newSingleThreadScheduledExecutor(new ThreadFactoryBuilder().setNameFormat( getName() + "-BuilderSchedulerPool-").setDaemon(true).build()); scheduler.scheduleAtFixedRate(new Runnable() { public void run() { int num = 0; for (Iterator<FutureBuildTask> i = tasks.values().iterator(); i.hasNext(); ) { if (Thread.currentThread().isInterrupted()) { return; } final FutureBuildTask task = i.next(); if (task.isExpired()) { i.remove(); try { cleanup(task); } catch (RuntimeException e) { LOG.error(e.getMessage(), e); } num++; } } if (num > 0) { LOG.debug("Remove {} expired tasks", num); } } }, 1, 1, TimeUnit.MINUTES); } else { throw new IllegalStateException("Already started"); } } protected void checkStarted() { if (!started.get()) { throw new IllegalStateException("Is not started yet."); } } /** * Stops Builder and releases any resources associated with the Builder. * <p/> * Sub-classes should invoke {@code super.stop} at the end of this method. */ @PreDestroy public void stop() { if (started.compareAndSet(true, false)) { boolean interrupted = false; scheduler.shutdownNow(); try { if (!scheduler.awaitTermination(5, TimeUnit.SECONDS)) { LOG.warn("Unable terminate scheduler"); } } catch (InterruptedException e) { interrupted = true; } executor.shutdown(); try { if (!executor.awaitTermination(10, TimeUnit.SECONDS)) { executor.shutdownNow(); if (!executor.awaitTermination(10, TimeUnit.SECONDS)) { LOG.warn("Unable terminate main pool"); } } } catch (InterruptedException e) { interrupted |= true; executor.shutdownNow(); } final java.io.File[] files = repository.listFiles(); if (files != null && files.length > 0) { for (java.io.File f : files) { boolean deleted; if (f.isDirectory()) { deleted = IoUtil.deleteRecursive(f); } else { deleted = f.delete(); } if (!deleted) { LOG.warn("Failed delete {}", f); } } } tasks.clear(); buildListeners.clear(); sourcesManager.stop(); // TODO: guice must do this if (interrupted) { Thread.currentThread().interrupt(); } } else { throw new IllegalStateException("Is not started yet."); } } public java.io.File getRepository() { checkStarted(); return repository; } public java.io.File getBuildDirectory() { checkStarted(); return builds; } public SourcesManager getSourcesManager() { checkStarted(); return sourcesManager; } public java.io.File getSourcesDirectory() { checkStarted(); return getSourcesManager().getDirectory(); } public int getNumberOfWorkers() { checkStarted(); return executor.getCorePoolSize(); } public int getNumberOfActiveWorkers() { checkStarted(); return executor.getActiveCount(); } public int getInternalQueueSize() { checkStarted(); return executor.getQueue().size(); } public int getMaxInternalQueueSize() { checkStarted(); return queueSize; } /** * Get global stats for this builder. * * @throws BuilderException * if any error occurs while getting builder metrics */ public List<BuilderMetric> getStats() throws BuilderException { List<BuilderMetric> global = new LinkedList<>(); final DtoFactory dtoFactory = DtoFactory.getInstance(); global.add(dtoFactory.createDto(BuilderMetric.class).withName(BuilderMetric.NUMBER_OF_WORKERS) .withValue(Integer.toString(getNumberOfWorkers()))); global.add(dtoFactory.createDto(BuilderMetric.class).withName(BuilderMetric.NUMBER_OF_ACTIVE_WORKERS) .withValue(Integer.toString(getNumberOfActiveWorkers()))); global.add(dtoFactory.createDto(BuilderMetric.class).withName(BuilderMetric.QUEUE_SIZE) .withValue(Integer.toString(getInternalQueueSize()))); global.add(dtoFactory.createDto(BuilderMetric.class).withName(BuilderMetric.MAX_QUEUE_SIZE) .withValue(Integer.toString(getMaxInternalQueueSize()))); return global; } /** * Add new BuildListener. * * @param listener * BuildListener * @return {@code true} if {@code listener} was added */ public boolean addBuildListener(BuildListener listener) { return buildListeners.add(listener); } /** * Remove BuildListener. * * @param listener * BuildListener * @return {@code true} if {@code listener} was removed */ public boolean removeBuildListener(BuildListener listener) { return buildListeners.remove(listener); } /** * Get all registered build listeners. Modifications to the returned {@code Set} will not affect the internal {@code Set}. * * @return all available download plugins */ public Set<BuildListener> getBuildListeners() { return new LinkedHashSet<>(buildListeners); } public BuilderConfigurationFactory getBuilderConfigurationFactory() { return new DefaultBuilderConfigurationFactory(this); } /** * Starts new build process. * * @param request * build request * @return build task * @throws BuilderException * if an error occurs */ public BuildTask perform(BuildRequest request) throws BuilderException { checkStarted(); final BuilderConfiguration configuration = getBuilderConfigurationFactory().createBuilderConfiguration(request); final java.io.File workDir = configuration.getWorkDir(); final java.io.File logFile = new java.io.File(workDir.getParentFile(), workDir.getName() + ".log"); final BuildLogger logger = createBuildLogger(configuration, logFile); return execute(configuration, logger); } /** * Starts new process of analysis dependencies. * * @param request * build request * @return build task * @throws BuilderException * if an error occurs */ public BuildTask perform(DependencyRequest request) throws BuilderException { checkStarted(); final BuilderConfiguration configuration = getBuilderConfigurationFactory().createBuilderConfiguration(request); final java.io.File workDir = configuration.getWorkDir(); final java.io.File logFile = new java.io.File(workDir.getParentFile(), workDir.getName() + ".log"); final BuildLogger logger = createBuildLogger(configuration, logFile); return execute(configuration, logger); } protected BuildTask execute(BuilderConfiguration configuration, BuildLogger logger) throws BuilderException { final CommandLine commandLine = createCommandLine(configuration); final BaseBuilderRequest request = configuration.getRequest(); final BuildLogger myLogger = new BuildLogsPublisher(logger, eventService, request.getId(), request.getWorkspace(), request.getProject()); final Callable<Boolean> callable = createTaskFor(commandLine, myLogger, request.getTimeout(), configuration); final Long internalId = buildIdSequence.getAndIncrement(); final BuildTask.Callback callback = new BuildTask.Callback() { @Override public void begin(BuildTask task) {} @Override public void done(BuildTask task) { final BaseBuilderRequest buildRequest = task.getConfiguration().getRequest(); eventService.publish(BuilderEvent.doneEvent(buildRequest.getId(), buildRequest.getWorkspace(), buildRequest.getProject())); try { myLogger.close(); LOG.debug("Close build logger {}", myLogger); } catch (IOException e) { LOG.error(e.getMessage(), e); } } }; final FutureBuildTask task = new FutureBuildTask(callable, internalId, commandLine, getName(), configuration, myLogger, callback); tasks.put(internalId, task); executor.execute(task); return task; } protected BuildLogger createBuildLogger(BuilderConfiguration buildConfiguration, java.io.File logFile) throws BuilderException { try { return new DefaultBuildLogger(logFile, "text/plain"); } catch (IOException e) { throw new BuilderException(e); } } protected Callable<Boolean> createTaskFor(final CommandLine commandLine, final BuildLogger logger, final long timeout, final BuilderConfiguration configuration) { return new Callable<Boolean>() { @Override public Boolean call() throws Exception { BaseBuilderRequest request = configuration.getRequest(); getSourcesManager() .getSources(logger, request.getWorkspace(), request.getProject(), request.getSourcesUrl(), configuration.getWorkDir()); // build effectively starts right after sources downloading is done eventService.publish(BuilderEvent.buildTimeStartedEvent(request.getId(), request.getWorkspace(), request.getProject(), System.currentTimeMillis())); eventService.publish(BuilderEvent.beginEvent(request.getId(), request.getWorkspace(), request.getProject())); StreamPump output = null; Watchdog watcher = null; int result = -1; try { ProcessBuilder processBuilder = new ProcessBuilder().command(commandLine.toShellCommand()).directory( configuration.getWorkDir()).redirectErrorStream(true); Process process = processBuilder.start(); if (timeout > 0) { watcher = new Watchdog(getName().toUpperCase() + "-WATCHDOG", timeout, TimeUnit.SECONDS); watcher.start(new CancellableProcessWrapper(process, new Cancellable.Callback() { @Override public void cancelled(Cancellable cancellable) { try { logger.writeLine("[ERROR] Your build has been shutdown due to timeout."); } catch (IOException e) { LOG.error(e.getMessage(), e); } } })); } output = new StreamPump(); output.start(process, logger); try { result = process.waitFor(); } catch (InterruptedException e) { Thread.interrupted(); // we interrupt thread when cancel task ProcessUtil.kill(process); } try { output.await(); // wait for logger } catch (InterruptedException e) { Thread.interrupted(); // we interrupt thread when cancel task, NOTE: logs may be incomplete } } finally { if (watcher != null) { watcher.stop(); } if (output != null) { output.stop(); } } LOG.debug("Done: {}, exit code: {}", commandLine, result); return result == 0; } }; } /** * Cleanup task. Cleanup means removing all local files which were created by build process, e.g logs, sources, build reports, etc. * <p/> * Sub-classes should invoke {@code super.cleanup} at the start of this method. * * @param task * build task */ protected void cleanup(BuildTask task) { final BuilderConfiguration configuration = task.getConfiguration(); final java.io.File workDir = configuration.getWorkDir(); if (workDir != null && workDir.exists()) { if (!IoUtil.deleteRecursive(workDir)) { LOG.warn("Unable delete directory {}", workDir); } } final java.io.File log = task.getBuildLogger().getFile(); if (log != null && log.exists()) { if (!log.delete()) { LOG.warn("Unable delete file {}", log); } } BuildResult result = null; try { result = task.getResult(); } catch (BuilderException e) { LOG.error("Skip cleanup of the task {}. Unable get task result.", task); } if (result != null) { List<java.io.File> artifacts = result.getResults(); if (!artifacts.isEmpty()) { for (java.io.File artifact : artifacts) { if (artifact.exists()) { if (!artifact.delete()) { LOG.warn("Unable delete file {}", artifact); } } } } if (result.hasBuildReport()) { java.io.File report = result.getBuildReport(); if (report != null && report.exists()) { if (!report.delete()) { LOG.warn("Unable delete file {}", report); } } } } final java.io.File buildDir = configuration.getBuildDir(); if (buildDir != null && buildDir.exists()) { if (!IoUtil.deleteRecursive(buildDir)) { LOG.warn("Unable delete directory {}", buildDir); } } } /** * Get build task by its {@code id}. Typically build process takes some time, so client start process of build or analyze dependencies * and periodically check is process already done. Client also may use {@link BuildListener} to be notified when build process starts * or ends. * * @param id * id of BuildTask * @return BuildTask * @throws NotFoundException * if id of BuildTask is invalid * @see #addBuildListener(BuildListener) * @see #removeBuildListener(BuildListener) */ public final BuildTask getBuildTask(Long id) throws NotFoundException { final FutureBuildTask task = tasks.get(id); if (task == null) { throw new NotFoundException(String.format("Invalid build task id: %d", id)); } return task; } /** * Get stats related to the specified build task. * * @throws NotFoundException * if id of BuildTask is invalid * @throws BuilderException * if any other error occurs * @see #getBuildTask(Long) */ public List<BuilderMetric> getStats(Long id) throws NotFoundException, BuilderException { return getStats(getBuildTask(id)); } protected List<BuilderMetric> getStats(BuildTask task) throws BuilderException { final List<BuilderMetric> result = new LinkedList<>(); final DtoFactory dtoFactory = DtoFactory.getInstance(); final long started = task.getStartTime(); final long ended = task.getEndTime(); if (started > 0) { result.add(dtoFactory.createDto(BuilderMetric.class).withName(BuilderMetric.START_TIME).withValue(Long.toString(started)) .withDescription("Time when build task was started")); if (ended <= 0) { long terminationTimeMillis = started + TimeUnit.SECONDS.toMillis(task.getConfiguration().getRequest().getTimeout()); result.add(dtoFactory.createDto(BuilderMetric.class).withName(BuilderMetric.TERMINATION_TIME) .withValue(Long.toString(terminationTimeMillis)) .withDescription("Time after that build task might be terminated")); } } if (ended > 0) { result.add(dtoFactory.createDto(BuilderMetric.class).withName(BuilderMetric.END_TIME).withValue(Long.toString(ended)) .withDescription("Time when build task was finished")); } final long runningTime = task.getRunningTime(); if (runningTime > 0) { result.add(dtoFactory.createDto(BuilderMetric.class).withName(BuilderMetric.RUNNING_TIME).withValue(Long.toString(runningTime)) .withDescription("Running time of build task")); } return result; } protected ExecutorService getExecutor() { return executor; } protected EventService getEventService() { return eventService; } protected class FutureBuildTask extends FutureTask<Boolean> implements BuildTask { private final Long id; private final CommandLine commandLine; private final String builder; private final BuilderConfiguration configuration; private final BuildLogger buildLogger; private final Callback callback; private BuildResult result; private long startTime; private long endTime; protected FutureBuildTask(Callable<Boolean> callable, Long id, CommandLine commandLine, String builder, BuilderConfiguration configuration, BuildLogger buildLogger, Callback callback) { super(callable); this.id = id; this.commandLine = commandLine; this.builder = builder; this.configuration = configuration; this.buildLogger = buildLogger; this.callback = callback; startTime = -1L; endTime = -1L; eventService.subscribe(new EventSubscriber<BuilderEvent>() { @Override public void onEvent(BuilderEvent event) { if (event.getType() == EventType.BUILD_TIME_STARTED) { final BuilderEvent.LoggedMessage message = event.getMessage(); startTime = Long.parseLong(message.getMessage()); } } }); } @Override public final Long getId() { return id; } @Override public String getBuilder() { return builder; } @Override public CommandLine getCommandLine() { return commandLine; } @Override public BuildLogger getBuildLogger() { return buildLogger; } @Override public void cancel() { super.cancel(true); } @Override public final BuildResult getResult() throws BuilderException { if (!isDone()) { return null; } if (result == null) { boolean successful; try { successful = super.get(); } catch (InterruptedException e) { // Should not happen since we checked is task done or not. Thread.currentThread().interrupt(); successful = false; } catch (ExecutionException e) { final Throwable cause = e.getCause(); if (cause instanceof Error) { throw (Error)cause; } else if (cause instanceof BuilderException) { throw (BuilderException)cause; } else if (cause instanceof ApiException) { throw new BuilderException(((ApiException)cause).getServiceError()); } else { throw new BuilderException(cause.getMessage(), cause); } } catch (CancellationException ce) { successful = false; } result = Builder.this.getTaskResult(this, successful); } return result; } @Override public BuilderConfiguration getConfiguration() { return configuration; } @Override public final synchronized boolean isStarted() { return startTime > 0; } @Override public final synchronized long getStartTime() { return startTime; } final synchronized void started() { if (callback != null) { // NOTE: important to do it in separate thread! getExecutor().execute(new Runnable() { @Override public void run() { try { // Need a bit time for process that post this task to finish. Problem arises if builder is easy loaded. In this // case BuildQueue gets notification event about starting build task even before process that posts build task // ends. This might make problem to see all phases of build process: // IN_QUEUE, IN_PROGRESS, SUCCESSFUL|FAILED|CANCELLED. Thread.sleep(300); } catch (InterruptedException ignored) { } callback.begin(FutureBuildTask.this); } }); } } @Override public final synchronized long getEndTime() { return endTime; } @Override public synchronized long getRunningTime() { return startTime > 0 ? endTime > 0 ? (endTime - startTime) : (System.currentTimeMillis() - startTime) : 0; } final synchronized void ended() { endTime = System.currentTimeMillis(); if (callback != null) { // NOTE: important to do it in separate thread! getExecutor().execute(new Runnable() { @Override public void run() { callback.done(FutureBuildTask.this); } }); } } synchronized boolean isExpired() { return endTime > 0 && (endTime + keepResultTimeMillis) < System.currentTimeMillis(); } @Override public String toString() { return "FutureBuildTask{" + "id=" + id + ", builder='" + builder + '\'' + ", workDir=" + configuration.getWorkDir() + '}'; } } private class MyThreadPoolExecutor extends ThreadPoolExecutor { private MyThreadPoolExecutor(int workerNumber, int queueSize) { super(workerNumber, workerNumber, 0L, TimeUnit.MILLISECONDS, new LinkedBlockingQueue<Runnable>(queueSize), new ThreadFactoryBuilder().setNameFormat(Builder.this.getName() + "-Builder-").setDaemon(true).build(), new ManyBuildTasksRejectedExecutionPolicy(new AbortPolicy())); } @Override protected void beforeExecute(Thread t, Runnable r) { if (r instanceof FutureBuildTask) { final FutureBuildTask futureBuildTask = (FutureBuildTask)r; for (BuildListener buildListener : getBuildListeners()) { try { buildListener.begin(futureBuildTask); } catch (RuntimeException e) { LOG.error(e.getMessage(), e); } } futureBuildTask.started(); } super.beforeExecute(t, r); } @Override protected void afterExecute(Runnable r, Throwable t) { super.afterExecute(r, t); if (r instanceof FutureBuildTask) { final FutureBuildTask futureBuildTask = (FutureBuildTask)r; // We know it is FutureBuildTask for (BuildListener buildListener : getBuildListeners()) { try { buildListener.end(futureBuildTask); } catch (RuntimeException e) { LOG.error(e.getMessage(), e); } } futureBuildTask.ended(); } } } }