/*************************** GO-LICENSE-START*********************************
* Copyright 2016 ThoughtWorks, Inc.
*
* 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.
* ************************GO-LICENSE-END***********************************/
package com.thoughtworks.go.buildsession;
import com.jezhumble.javasysmon.JavaSysMon;
import com.thoughtworks.go.domain.*;
import com.thoughtworks.go.util.Clock;
import com.thoughtworks.go.util.GoConstants;
import com.thoughtworks.go.util.HttpService;
import com.thoughtworks.go.util.command.ProcessOutputStreamConsumer;
import com.thoughtworks.go.util.command.SafeOutputStreamConsumer;
import com.thoughtworks.go.util.command.TaggedStreamConsumer;
import org.apache.commons.lang.text.StrLookup;
import org.apache.commons.lang.text.StrSubstitutor;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.File;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.*;
import static com.thoughtworks.go.util.ExceptionUtils.bomb;
import static com.thoughtworks.go.util.ExceptionUtils.messageOf;
import static com.thoughtworks.go.util.FileUtil.applyBaseDirIfRelative;
import static java.lang.String.format;
public class BuildSession {
private static final Logger LOG = LoggerFactory.getLogger(BuildSession.class);
private final Map<String, String> envs;
private final Map<String, String> secretSubstitutions;
private final String buildId;
private final BuildStateReporter buildStateReporter;
private final TaggedStreamConsumer console;
private final DownloadAction downloadAction;
private final ExecutorService executorService;
private File workingDir;
private JobResult buildResult;
private final StrLookup buildVariables;
private ArtifactsRepository artifactsRepository;
private HttpService httpService;
private Clock clock;
private final CountDownLatch doneLatch;
private CountDownLatch cancelLatch;
private static Map<String, BuildCommandExecutor> executors = new HashMap<>();
static {
executors.put("echo", new EchoCommandExecutor());
executors.put("downloadDir", new DownloadDirCommandExecutor());
executors.put("downloadFile", new DownloadFileCommandExecutor());
executors.put("uploadArtifact", new UploadArtifactCommandExecutor());
executors.put("secret", new SecretCommandExecutor());
executors.put("export", new ExportCommandExecutor());
executors.put("compose", new ComposeCommandExecutor());
executors.put("fail", new FailCommandExecutor());
executors.put("mkdirs", new MkdirsCommandExecutor());
executors.put("cleandir", new CleandirCommandExecutor());
executors.put("exec", new ExecCommandExecutor());
executors.put("test", new TestCommandExecutor());
executors.put("reportCurrentStatus", new ReportCurrentStatusCommandExecutor());
executors.put("reportCompleting", new ReportCompletingCommandExecutor());
executors.put("generateTestReport", new GenerateTestReportCommandExecutor());
executors.put("generateProperty", new GeneratePropertyCommandExecutor());
executors.put("error", new ErrorCommandExecutor());
}
public BuildSession(String buildId, BuildStateReporter buildStateReporter, TaggedStreamConsumer console, StrLookup buildVariables, ArtifactsRepository artifactsRepository, HttpService httpService, Clock clock, File workingDir) {
this.buildId = buildId;
this.buildStateReporter = buildStateReporter;
this.console = console;
this.buildVariables = buildVariables;
this.artifactsRepository = artifactsRepository;
this.httpService = httpService;
this.clock = clock;
this.workingDir = workingDir;
this.envs = new HashMap<>();
this.secretSubstitutions = new HashMap<>();
this.buildResult = JobResult.Passed;
this.doneLatch = new CountDownLatch(1);
this.cancelLatch = new CountDownLatch(1);
this.downloadAction = new DownloadAction(httpService, getPublisher(), clock);
this.executorService = Executors.newCachedThreadPool();
}
public void setEnv(String name, String value) {
if (value == null) {
value = "";
}
envs.put(name, value);
}
/**
* Cancel build and wait for build session done
*
* @return {@code true} if the build session is done and {@code false}
* if time out happens
*/
public boolean cancel(int timeout, TimeUnit timeoutUnit) throws InterruptedException {
if (isCanceled()) {
return true;
}
cancelLatch.countDown();
try {
return doneLatch.await(timeout, timeoutUnit);
} finally {
new JavaSysMon().infanticide();
}
}
public JobResult build(BuildCommand command) {
if (isDone()) {
throw bomb("Shall not reuse a build session!");
}
try {
processCommand(command);
return buildResult;
} finally {
try {
buildStateReporter.reportCompleted(buildId, buildResult);
} catch (Exception e) {
reportException(e);
}
try {
executorService.shutdownNow();
} catch (Exception e) {
reportException(e);
}
doneLatch.countDown();
}
}
boolean processCommand(BuildCommand command) {
if (isCanceled()) {
buildResult = JobResult.Cancelled;
return false;
}
LOG.debug("Processing build command {}", command.getName());
BuildCommandExecutor executor = executors.get(command.getName());
if (executor == null) {
LOG.error("Unknown command: " + command.getName());
println("error: build command " + command.getName() + " is not supported. Please upgrade GoCD agent");
buildResult = JobResult.Failed;
return false;
}
boolean success = doProcess(command, executor);
if (isCanceled()) {
buildResult = JobResult.Cancelled;
return false;
}
if (!success) {
this.buildResult = JobResult.Failed;
}
return success;
}
private boolean doProcess(BuildCommand command, BuildCommandExecutor executor) {
BuildCommand onCancelCommand = command.getOnCancel();
try {
if (("passed".equals(command.getRunIfConfig()) && buildResult.isFailed())
|| ("failed".equals(command.getRunIfConfig()) && this.buildResult.isPassed())) {
return true;
}
BuildCommand test = command.getTest();
if (test != null) {
if (newTestingSession(console).build(test) != JobResult.Passed) {
return true;
}
}
if (isCanceled()) {
return false;
}
return executor.execute(command, this);
} catch (Exception e) {
reportException(e);
return false;
} finally {
if (isCanceled() && onCancelCommand != null) {
newCancelSession().build(onCancelCommand);
}
}
}
File resolveRelativeDir(String... dirs) {
if (dirs.length == 0) {
return workingDir;
}
File result = new File(dirs[dirs.length - 1]);
for (int i = dirs.length - 2; i >= 0; i--) {
result = applyBaseDirIfRelative(new File(dirs[i]), result);
}
return applyBaseDirIfRelative(workingDir, result);
}
Future<?> submitRunnable(Runnable runnable) {
return executorService.submit(runnable);
}
void addSecret(String secret, String substitution) {
if (substitution == null) {
substitution = "******";
}
this.secretSubstitutions.put(secret, substitution);
}
Map<String, String> getEnvs() {
return Collections.unmodifiableMap(envs);
}
void printlnSafely(String line) {
newSafeConsole().stdOutput(line);
}
void println(String line) {
getPublisher().consumeLine(line);
}
public void printlnWithPrefix(String line) {
this.println(String.format("[%s] %s", GoConstants.PRODUCT_NAME, line));
}
String buildVariableSubstitute(String str) {
return new StrSubstitutor(buildVariables).replace(str);
}
void upload(File file, String dest) {
getPublisher().upload(file, dest);
}
ProcessOutputStreamConsumer processOutputStreamConsumer() {
return new ProcessOutputStreamConsumer<>(console, console);
}
Map<String, String> getSecretSubstitutions() {
return Collections.unmodifiableMap(secretSubstitutions);
}
void waitUntilCanceled() throws InterruptedException {
cancelLatch.await();
}
void reportBuildStatus(String status) {
buildStateReporter.reportBuildStatus(buildId, JobState.valueOf(status));
}
void reportCompleting() {
buildStateReporter.reportCompleting(buildId, buildResult);
}
BuildSession newTestingSession(TaggedStreamConsumer console) {
BuildSession buildSession = new BuildSession(
buildId, new UncaringBuildStateReport(), console, buildVariables, artifactsRepository, httpService, clock, workingDir);
buildSession.cancelLatch = this.cancelLatch;
return buildSession;
}
void download(FetchHandler handler, String url, ChecksumFileHandler checksumFileHandler, String checksumUrl) {
try {
if (checksumFileHandler != null) {
downloadAction.perform(checksumUrl, checksumFileHandler);
handler.useArtifactMd5Checksums(checksumFileHandler.getArtifactMd5Checksums());
}
downloadAction.perform(url, handler);
} catch (InterruptedException e) {
throw new RuntimeException("download interrupted");
}
}
BuildSessionGoPublisher getPublisher() {
return new BuildSessionGoPublisher(console, artifactsRepository, buildId);
}
private BuildSession newCancelSession() {
return new BuildSession(buildId, new UncaringBuildStateReport(),
console, buildVariables, artifactsRepository, httpService, clock, workingDir);
}
private boolean isDone() {
return doneLatch.getCount() < 1;
}
private boolean isCanceled() {
return cancelLatch.getCount() < 1;
}
private SafeOutputStreamConsumer newSafeConsole() {
ProcessOutputStreamConsumer processConsumer = new ProcessOutputStreamConsumer<>(console, console);
SafeOutputStreamConsumer streamConsumer = new SafeOutputStreamConsumer(processConsumer);
for (String secret : secretSubstitutions.keySet()) {
streamConsumer.addSecret(new SecretSubstitution(secret, secretSubstitutions.get(secret)));
}
return streamConsumer;
}
private void reportException(Exception e) {
String msg = messageOf(e);
try {
LOG.error(msg, e);
printlnSafely(msg);
} catch (Exception reportException) {
LOG.error(format("Unable to report error message - %s.", messageOf(e)), reportException);
}
}
}