/* * Copyright 2017 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package org.gradle.tooling.internal.consumer; import org.gradle.internal.UncheckedException; import org.gradle.internal.logging.progress.ProgressLogger; import org.gradle.internal.logging.progress.ProgressLoggerFactory; import org.gradle.internal.time.TimeProvider; import org.gradle.internal.time.TrueTimeProvider; import org.gradle.tooling.events.OperationDescriptor; import org.gradle.tooling.events.OperationResult; import org.gradle.tooling.events.StatusEvent; import org.gradle.tooling.events.internal.ConsumerOperationDescriptor; import org.gradle.tooling.events.internal.DefaultFinishEvent; import org.gradle.tooling.events.internal.DefaultOperationFailureResult; import org.gradle.tooling.events.internal.DefaultOperationSuccessResult; import org.gradle.tooling.events.internal.DefaultStartEvent; import org.gradle.tooling.events.internal.DefaultStatusEvent; import org.gradle.tooling.internal.protocol.InternalBuildProgressListener; import org.gradle.util.GradleVersion; import org.gradle.wrapper.Download; import org.gradle.wrapper.DownloadProgressListener; import org.gradle.wrapper.IDownload; import org.gradle.wrapper.Install; import org.gradle.wrapper.Logger; import org.gradle.wrapper.PathAssembler; import org.gradle.wrapper.WrapperConfiguration; import java.io.File; import java.net.URI; import java.util.Collections; import java.util.List; import java.util.concurrent.CancellationException; import java.util.concurrent.atomic.AtomicReference; public class DistributionInstaller { private static final String APP_NAME = "Gradle Tooling API"; private static final InternalBuildProgressListener NO_OP = new NoOpListener(); private final ProgressLoggerFactory progressLoggerFactory; private final InternalBuildProgressListener buildProgressListener; private final TimeProvider timeProvider; private final AtomicReference<InternalBuildProgressListener> currentListener = new AtomicReference<InternalBuildProgressListener>(NO_OP); // Protects the following state private final Object lock = new Object(); private boolean completed; private boolean cancelled; private Throwable failure; public DistributionInstaller(ProgressLoggerFactory progressLoggerFactory, InternalBuildProgressListener buildProgressListener) { this.progressLoggerFactory = progressLoggerFactory; this.buildProgressListener = buildProgressListener; this.timeProvider = new TrueTimeProvider(); } /** * Installs the distribution and returns the result. */ public File install(File userHomeDir, WrapperConfiguration wrapperConfiguration) throws Exception { Install install = new Install(new Logger(false), new AsyncDownload(), new PathAssembler(userHomeDir)); return install.createDist(wrapperConfiguration); } /** * Cancels the current installation, if running. */ public void cancel() { synchronized (lock) { cancelled = true; lock.notifyAll(); } } private void doDownload(URI address, File destination) throws Exception { String displayName = "Download " + address; OperationDescriptor descriptor = new ConsumerOperationDescriptor(displayName); long startTime = timeProvider.getCurrentTime(); buildProgressListener.onEvent(new DefaultStartEvent(startTime, displayName + " started", descriptor)); Throwable failure = null; try { withProgressLogging(address, destination, descriptor); } catch (Throwable t) { failure = t; } long endTime = timeProvider.getCurrentTime(); OperationResult result = failure == null ? new DefaultOperationSuccessResult(startTime, endTime) : new DefaultOperationFailureResult(startTime, endTime, Collections.singletonList(DefaultFailure.fromThrowable(failure))); buildProgressListener.onEvent(new DefaultFinishEvent(endTime, displayName + " finished", descriptor, result)); if (failure != null) { if (failure instanceof Exception) { throw (Exception) failure; } throw UncheckedException.throwAsUncheckedException(failure); } } private void withProgressLogging(URI address, File destination, OperationDescriptor operationDescriptor) throws Throwable { ProgressLogger progressLogger = progressLoggerFactory.newOperation(DistributionInstaller.class); progressLogger.setDescription("Download " + address); progressLogger.started(); try { withAsyncDownload(address, destination, operationDescriptor); } finally { progressLogger.completed(); } } private void withAsyncDownload(final URI address, final File destination, final OperationDescriptor operationDescriptor) throws Throwable { currentListener.set(buildProgressListener); try { // Start the download in another thread and wait for the result Thread thread = new Thread("Distribution download") { @Override public void run() { try { new Download(new Logger(false), new ForwardingDownloadProgressListener(operationDescriptor), APP_NAME, GradleVersion.current().getVersion()).download(address, destination); } catch (Throwable t) { synchronized (lock) { failure = t; } } finally { synchronized (lock) { completed = true; lock.notifyAll(); } } } }; thread.setDaemon(true); thread.start(); synchronized (lock) { while (!completed && !cancelled) { try { lock.wait(); } catch (InterruptedException e) { throw UncheckedException.throwAsUncheckedException(e); } } if (failure != null) { throw failure; } if (cancelled) { // When cancelled, try to stop the download thread but don't attempt to wait for it to complete // Could possibly loop here for a while trying to force the thread to exit thread.interrupt(); throw new CancellationException(); } } } finally { // The download thread may still be running. Ignore any further status events from it currentListener.set(NO_OP); } } private static class NoOpListener implements InternalBuildProgressListener { @Override public void onEvent(Object event) { } @Override public List<String> getSubscribedOperations() { return Collections.emptyList(); } } private class ForwardingDownloadProgressListener implements DownloadProgressListener { private final OperationDescriptor descriptor; ForwardingDownloadProgressListener(OperationDescriptor descriptor) { this.descriptor = descriptor; } @Override public void downloadStatusChanged(URI address, long contentLength, long downloaded) { String eventDisplayName = descriptor.getDisplayName() + " " + downloaded + "/" + contentLength + " bytes downloaded"; StatusEvent statusEvent = new DefaultStatusEvent(timeProvider.getCurrentTime(), eventDisplayName, descriptor, contentLength, downloaded, "bytes"); // This is called from the download thread. Only forward the events when not cancelled currentListener.get().onEvent(statusEvent); } } private class AsyncDownload implements IDownload { @Override public void download(URI address, File destination) throws Exception { synchronized (lock) { doDownload(address, destination); } } } }