blob: 95279d5df6d2c7d111905a92966f2fe9ae9175f8 [file] [log] [blame]
/*
* Copyright 2000-2014 JetBrains s.r.o.
*
* 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.jetbrains.jps.cmdline;
import com.intellij.openapi.diagnostic.Logger;
import com.intellij.openapi.util.Pair;
import com.intellij.openapi.util.Ref;
import com.intellij.openapi.util.io.BufferExposingByteArrayOutputStream;
import com.intellij.openapi.util.io.FileSystemUtil;
import com.intellij.openapi.util.io.FileUtil;
import com.intellij.openapi.util.text.StringUtil;
import com.intellij.util.concurrency.SequentialTaskExecutor;
import com.intellij.util.io.DataOutputStream;
import io.netty.channel.Channel;
import org.jetbrains.annotations.Nullable;
import org.jetbrains.jps.TimingLog;
import org.jetbrains.jps.api.*;
import org.jetbrains.jps.builders.*;
import org.jetbrains.jps.builders.java.JavaModuleBuildTargetType;
import org.jetbrains.jps.builders.java.dependencyView.Callbacks;
import org.jetbrains.jps.incremental.MessageHandler;
import org.jetbrains.jps.incremental.TargetTypeRegistry;
import org.jetbrains.jps.incremental.Utils;
import org.jetbrains.jps.incremental.fs.BuildFSState;
import org.jetbrains.jps.incremental.fs.FSState;
import org.jetbrains.jps.incremental.messages.*;
import org.jetbrains.jps.incremental.storage.Timestamps;
import org.jetbrains.jps.model.module.JpsModule;
import org.jetbrains.jps.service.SharedThreadPool;
import java.io.*;
import java.util.*;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.Future;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
import java.util.concurrent.atomic.AtomicBoolean;
import static org.jetbrains.jps.api.CmdlineRemoteProto.Message.ControllerMessage.ParametersMessage.TargetTypeBuildScope;
/**
* @author Eugene Zhuravlev
* Date: 4/17/12
*/
final class BuildSession implements Runnable, CanceledStatus {
private static final Logger LOG = Logger.getInstance("#org.jetbrains.jps.cmdline.BuildSession");
private static final String FS_STATE_FILE = "fs_state.dat";
private final UUID mySessionId;
private final Channel myChannel;
private volatile boolean myCanceled = false;
private final String myProjectPath;
@Nullable
private CmdlineRemoteProto.Message.ControllerMessage.FSEvent myInitialFSDelta;
// state
private final EventsProcessor myEventsProcessor = new EventsProcessor();
private volatile long myLastEventOrdinal;
private volatile ProjectDescriptor myProjectDescriptor;
private final Map<Pair<String, String>, ConstantSearchFuture> mySearchTasks = Collections.synchronizedMap(new HashMap<Pair<String, String>, ConstantSearchFuture>());
private final ConstantSearch myConstantSearch = new ConstantSearch();
private final BuildRunner myBuildRunner;
private final boolean myForceModelLoading;
private final BuildType myBuildType;
private final List<TargetTypeBuildScope> myScopes;
BuildSession(UUID sessionId,
Channel channel,
CmdlineRemoteProto.Message.ControllerMessage.ParametersMessage params,
@Nullable CmdlineRemoteProto.Message.ControllerMessage.FSEvent delta) {
mySessionId = sessionId;
myChannel = channel;
final CmdlineRemoteProto.Message.ControllerMessage.GlobalSettings globals = params.getGlobalSettings();
myProjectPath = FileUtil.toCanonicalPath(params.getProjectId());
String globalOptionsPath = FileUtil.toCanonicalPath(globals.getGlobalOptionsPath());
myBuildType = convertCompileType(params.getBuildType());
myScopes = params.getScopeList();
List<String> filePaths = params.getFilePathList();
Map<String, String> builderParams = new HashMap<String, String>();
for (CmdlineRemoteProto.Message.KeyValuePair pair : params.getBuilderParameterList()) {
builderParams.put(pair.getKey(), pair.getValue());
}
myInitialFSDelta = delta;
JpsModelLoaderImpl loader = new JpsModelLoaderImpl(myProjectPath, globalOptionsPath, null);
myForceModelLoading = Boolean.parseBoolean(builderParams.get(BuildParametersKeys.FORCE_MODEL_LOADING));
myBuildRunner = new BuildRunner(loader, filePaths, builderParams);
}
@Override
public void run() {
Throwable error = null;
final Ref<Boolean> hasErrors = new Ref<Boolean>(false);
final Ref<Boolean> doneSomething = new Ref<Boolean>(false);
try {
ProfilingHelper profilingHelper = null;
if (Utils.IS_PROFILING_MODE) {
profilingHelper = new ProfilingHelper();
profilingHelper.startProfiling();
}
runBuild(new MessageHandler() {
@Override
public void processMessage(BuildMessage buildMessage) {
final CmdlineRemoteProto.Message.BuilderMessage response;
if (buildMessage instanceof FileGeneratedEvent) {
final Collection<Pair<String, String>> paths = ((FileGeneratedEvent)buildMessage).getPaths();
response = !paths.isEmpty() ? CmdlineProtoUtil.createFileGeneratedEvent(paths) : null;
}
else if (buildMessage instanceof DoneSomethingNotification) {
doneSomething.set(true);
response = null;
}
else if (buildMessage instanceof CompilerMessage) {
doneSomething.set(true);
final CompilerMessage compilerMessage = (CompilerMessage)buildMessage;
final String compilerName = compilerMessage.getCompilerName();
final String text = !StringUtil.isEmptyOrSpaces(compilerName)? compilerName + ": " + compilerMessage.getMessageText() : compilerMessage.getMessageText();
final BuildMessage.Kind kind = compilerMessage.getKind();
if (kind == BuildMessage.Kind.ERROR) {
hasErrors.set(true);
}
response = CmdlineProtoUtil.createCompileMessage(
kind, text, compilerMessage.getSourcePath(),
compilerMessage.getProblemBeginOffset(), compilerMessage.getProblemEndOffset(),
compilerMessage.getProblemLocationOffset(), compilerMessage.getLine(), compilerMessage.getColumn(),
-1.0f);
}
else if (buildMessage instanceof CustomBuilderMessage) {
CustomBuilderMessage builderMessage = (CustomBuilderMessage)buildMessage;
response = CmdlineProtoUtil.createCustomBuilderMessage(builderMessage.getBuilderId(), builderMessage.getMessageType(), builderMessage.getMessageText());
}
else if (buildMessage instanceof BuilderStatisticsMessage) {
BuilderStatisticsMessage message = (BuilderStatisticsMessage)buildMessage;
LOG.info("Build duration: '" + message.getBuilderName() + "' builder took " + message.getElapsedTimeMs() + " ms");
response = null;
}
else if (!(buildMessage instanceof BuildingTargetProgressMessage)) {
float done = -1.0f;
if (buildMessage instanceof ProgressMessage) {
done = ((ProgressMessage)buildMessage).getDone();
}
response = CmdlineProtoUtil.createCompileProgressMessageResponse(buildMessage.getMessageText(), done);
}
else {
response = null;
}
if (response != null) {
myChannel.writeAndFlush(CmdlineProtoUtil.toMessage(mySessionId, response));
}
}
}, this);
if (profilingHelper != null) {
profilingHelper.stopProfiling();
}
}
catch (Throwable e) {
LOG.info(e);
error = e;
}
finally {
finishBuild(error, hasErrors.get(), doneSomething.get());
}
}
private void runBuild(final MessageHandler msgHandler, CanceledStatus cs) throws Throwable{
final File dataStorageRoot = Utils.getDataStorageRoot(myProjectPath);
if (dataStorageRoot == null) {
msgHandler.processMessage(new CompilerMessage("build", BuildMessage.Kind.ERROR, "Cannot determine build data storage root for project " + myProjectPath));
return;
}
if (!dataStorageRoot.exists()) {
// invoked the very first time for this project
myBuildRunner.setForceCleanCaches(true);
}
final DataInputStream fsStateStream = createFSDataStream(dataStorageRoot);
if (fsStateStream != null) {
// optimization: check whether we can skip the build
final boolean hasWorkToDoWithModules = fsStateStream.readBoolean();
if (!myForceModelLoading && (myBuildType == BuildType.BUILD || myBuildType == BuildType.UP_TO_DATE_CHECK) && !hasWorkToDoWithModules
&& scopeContainsModulesOnlyForIncrementalMake(myScopes) && !containsChanges(myInitialFSDelta)) {
updateFsStateOnDisk(dataStorageRoot, fsStateStream, myInitialFSDelta.getOrdinal());
return;
}
}
final BuildFSState fsState = new BuildFSState(false);
try {
final ProjectDescriptor pd = myBuildRunner.load(msgHandler, dataStorageRoot, fsState);
TimingLog.LOG.debug("Project descriptor loaded");
myProjectDescriptor = pd;
if (fsStateStream != null) {
try {
try {
fsState.load(fsStateStream, pd.getModel(), pd.getBuildRootIndex());
applyFSEvent(pd, myInitialFSDelta, false);
TimingLog.LOG.debug("FS Delta loaded");
}
finally {
fsStateStream.close();
}
}
catch (Throwable e) {
LOG.error(e);
fsState.clearAll();
}
}
myLastEventOrdinal = myInitialFSDelta != null? myInitialFSDelta.getOrdinal() : 0L;
// free memory
myInitialFSDelta = null;
// ensure events from controller are processed after FSState initialization
myEventsProcessor.startProcessing();
myBuildRunner.runBuild(pd, cs, myConstantSearch, msgHandler, myBuildType, myScopes, false);
TimingLog.LOG.debug("Build finished");
}
finally {
saveData(fsState, dataStorageRoot);
}
}
private static boolean scopeContainsModulesOnlyForIncrementalMake(List<TargetTypeBuildScope> scopes) {
TargetTypeRegistry typeRegistry = null;
for (TargetTypeBuildScope scope : scopes) {
if (scope.getForceBuild()) return false;
final String typeId = scope.getTypeId();
if (isJavaModuleBuildType(typeId)) { // fast check
continue;
}
if (typeRegistry == null) {
// lazy init
typeRegistry = TargetTypeRegistry.getInstance();
}
final BuildTargetType<?> targetType = typeRegistry.getTargetType(typeId);
if (targetType != null && !(targetType instanceof ModuleBasedBuildTargetType)) {
return false;
}
}
return true;
}
private static boolean isJavaModuleBuildType(String typeId) {
for (JavaModuleBuildTargetType moduleBuildTargetType : JavaModuleBuildTargetType.ALL_TYPES) {
if (moduleBuildTargetType.getTypeId().equals(typeId)) {
return true;
}
}
return false;
}
private void saveData(final BuildFSState fsState, File dataStorageRoot) {
final boolean wasInterrupted = Thread.interrupted();
try {
saveFsState(dataStorageRoot, fsState);
final ProjectDescriptor pd = myProjectDescriptor;
if (pd != null) {
pd.release();
}
}
finally {
if (wasInterrupted) {
Thread.currentThread().interrupt();
}
}
}
public void processFSEvent(final CmdlineRemoteProto.Message.ControllerMessage.FSEvent event) {
myEventsProcessor.submit(new Runnable() {
@Override
public void run() {
try {
applyFSEvent(myProjectDescriptor, event, true);
myLastEventOrdinal += 1;
}
catch (IOException e) {
LOG.error(e);
}
}
});
}
public void processConstantSearchResult(CmdlineRemoteProto.Message.ControllerMessage.ConstantSearchResult result) {
final ConstantSearchFuture future = mySearchTasks.remove(Pair.create(result.getOwnerClassName(), result.getFieldName()));
if (future != null) {
if (result.getIsSuccess()) {
final List<String> paths = result.getPathList();
final List<File> files = new ArrayList<File>(paths.size());
for (String path : paths) {
files.add(new File(path));
}
future.setResult(files);
LOG.debug("Constant search result: " + files.size() + " affected files found");
}
else {
future.setDone();
LOG.debug("Constant search failed");
}
}
}
private static void applyFSEvent(ProjectDescriptor pd, @Nullable CmdlineRemoteProto.Message.ControllerMessage.FSEvent event,
final boolean saveEventStamp) throws IOException {
if (event == null) {
return;
}
final Timestamps timestamps = pd.timestamps.getStorage();
boolean cacheCleared = false;
for (String deleted : event.getDeletedPathsList()) {
final File file = new File(deleted);
Collection<BuildRootDescriptor> descriptor = pd.getBuildRootIndex().findAllParentDescriptors(file, null, null);
if (!descriptor.isEmpty()) {
if (!cacheCleared) {
pd.getFSCache().clear();
cacheCleared = true;
}
if (LOG.isDebugEnabled()) {
LOG.debug("Applying deleted path from fs event: " + file.getPath());
}
for (BuildRootDescriptor rootDescriptor : descriptor) {
pd.fsState.registerDeleted(rootDescriptor.getTarget(), file, timestamps);
}
}
else {
if (LOG.isDebugEnabled()) {
LOG.debug("Skipping deleted path: " + file.getPath());
}
}
}
for (String changed : event.getChangedPathsList()) {
final File file = new File(changed);
Collection<BuildRootDescriptor> descriptors = pd.getBuildRootIndex().findAllParentDescriptors(file, null, null);
if (!descriptors.isEmpty()) {
if (LOG.isDebugEnabled()) {
LOG.debug("Applying dirty path from fs event: " + changed);
}
long fileStamp = -1L;
for (BuildRootDescriptor descriptor : descriptors) {
if (!descriptor.isGenerated()) { // ignore generates sources as they are processed at the time of generation
if (fileStamp == -1L) {
fileStamp = FileSystemUtil.lastModified(file); // lazy init
}
final long stamp = timestamps.getStamp(file, descriptor.getTarget());
if (stamp != fileStamp) {
if (!cacheCleared) {
pd.getFSCache().clear();
cacheCleared = true;
}
pd.fsState.markDirty(null, file, descriptor, timestamps, saveEventStamp);
}
else {
if (LOG.isDebugEnabled()) {
LOG.debug(descriptor.getTarget() + ": Path considered up-to-date: " + changed + "; timestamp= " + stamp);
}
}
}
}
}
else {
if (LOG.isDebugEnabled()) {
LOG.debug("Skipping dirty path: " + file.getPath());
}
}
}
}
private static void updateFsStateOnDisk(File dataStorageRoot, DataInputStream original, final long ordinal) {
final File file = new File(dataStorageRoot, FS_STATE_FILE);
try {
final BufferExposingByteArrayOutputStream bytes = new BufferExposingByteArrayOutputStream();
final DataOutputStream out = new DataOutputStream(bytes);
try {
out.writeInt(FSState.VERSION);
out.writeLong(ordinal);
out.writeBoolean(false);
while (true) {
final int b = original.read();
if (b == -1) {
break;
}
out.write(b);
}
}
finally {
out.close();
}
saveOnDisk(bytes, file);
}
catch (Throwable e) {
LOG.error(e);
FileUtil.delete(file);
}
}
private void saveFsState(File dataStorageRoot, BuildFSState state) {
final ProjectDescriptor pd = myProjectDescriptor;
final File file = new File(dataStorageRoot, FS_STATE_FILE);
try {
final BufferExposingByteArrayOutputStream bytes = new BufferExposingByteArrayOutputStream();
final DataOutputStream out = new DataOutputStream(bytes);
try {
out.writeInt(FSState.VERSION);
out.writeLong(myLastEventOrdinal);
out.writeBoolean(hasWorkToDo(state, pd));
state.save(out);
}
finally {
out.close();
}
saveOnDisk(bytes, file);
}
catch (Throwable e) {
LOG.error(e);
FileUtil.delete(file);
}
}
private static boolean hasWorkToDo(BuildFSState state, ProjectDescriptor pd) {
final BuildTargetIndex targetIndex = pd.getBuildTargetIndex();
for (JpsModule module : pd.getProject().getModules()) {
for (ModuleBasedTarget<?> target : targetIndex.getModuleBasedTargets(module, BuildTargetRegistry.ModuleTargetSelector.ALL)) {
if (state.hasWorkToDo(target)) {
return true;
}
}
}
return false;
}
private static void saveOnDisk(BufferExposingByteArrayOutputStream bytes, final File file) throws IOException {
FileOutputStream fos = null;
try {
//noinspection IOResourceOpenedButNotSafelyClosed
fos = new FileOutputStream(file);
}
catch (FileNotFoundException ignored) {
FileUtil.createIfDoesntExist(file);
}
if (fos == null) {
fos = new FileOutputStream(file);
}
try {
fos.write(bytes.getInternalBuffer(), 0, bytes.size());
}
finally {
fos.close();
}
}
@Nullable
private DataInputStream createFSDataStream(File dataStorageRoot) {
if (myInitialFSDelta == null) {
// this will force FS rescan
return null;
}
try {
final File file = new File(dataStorageRoot, FS_STATE_FILE);
byte[] bytes;
final InputStream fs = new FileInputStream(file);
try {
bytes = FileUtil.loadBytes(fs, (int)file.length());
}
finally {
fs.close();
}
final DataInputStream in = new DataInputStream(new ByteArrayInputStream(bytes));
final int version = in.readInt();
if (version != FSState.VERSION) {
return null;
}
final long savedOrdinal = in.readLong();
if (savedOrdinal + 1L != myInitialFSDelta.getOrdinal()) {
return null;
}
return in;
}
catch (FileNotFoundException ignored) {
}
catch (Throwable e) {
LOG.error(e);
}
return null;
}
private static boolean containsChanges(CmdlineRemoteProto.Message.ControllerMessage.FSEvent event) {
return event.getChangedPathsCount() != 0 || event.getDeletedPathsCount() != 0;
}
private void finishBuild(Throwable error, boolean hadBuildErrors, boolean doneSomething) {
CmdlineRemoteProto.Message lastMessage = null;
try {
if (error != null) {
Throwable cause = error.getCause();
if (cause == null) {
cause = error;
}
final ByteArrayOutputStream out = new ByteArrayOutputStream();
final PrintStream stream = new PrintStream(out);
try {
cause.printStackTrace(stream);
}
finally {
stream.close();
}
final StringBuilder messageText = new StringBuilder();
messageText.append("Internal error: (").append(cause.getClass().getName()).append(") ").append(cause.getMessage());
final String trace = out.toString();
if (!trace.isEmpty()) {
messageText.append("\n").append(trace);
}
lastMessage = CmdlineProtoUtil.toMessage(mySessionId, CmdlineProtoUtil.createFailure(messageText.toString(), cause));
}
else {
CmdlineRemoteProto.Message.BuilderMessage.BuildEvent.Status status = CmdlineRemoteProto.Message.BuilderMessage.BuildEvent.Status.SUCCESS;
if (myCanceled) {
status = CmdlineRemoteProto.Message.BuilderMessage.BuildEvent.Status.CANCELED;
}
else if (hadBuildErrors) {
status = CmdlineRemoteProto.Message.BuilderMessage.BuildEvent.Status.ERRORS;
}
else if (!doneSomething){
status = CmdlineRemoteProto.Message.BuilderMessage.BuildEvent.Status.UP_TO_DATE;
}
lastMessage = CmdlineProtoUtil.toMessage(mySessionId, CmdlineProtoUtil.createBuildCompletedEvent("build completed", status));
}
}
catch (Throwable e) {
lastMessage = CmdlineProtoUtil.toMessage(mySessionId, CmdlineProtoUtil.createFailure(e.getMessage(), e));
}
finally {
try {
myChannel.writeAndFlush(lastMessage).await();
}
catch (InterruptedException e) {
LOG.info(e);
}
}
}
public void cancel() {
myCanceled = true;
}
@Override
public boolean isCanceled() {
return myCanceled;
}
private static BuildType convertCompileType(CmdlineRemoteProto.Message.ControllerMessage.ParametersMessage.Type compileType) {
switch (compileType) {
case CLEAN: return BuildType.CLEAN;
case BUILD: return BuildType.BUILD;
case UP_TO_DATE_CHECK: return BuildType.UP_TO_DATE_CHECK;
}
return BuildType.BUILD;
}
private static class EventsProcessor extends SequentialTaskExecutor {
private final AtomicBoolean myProcessingEnabled = new AtomicBoolean(false);
EventsProcessor() {
super(SharedThreadPool.getInstance());
}
public void startProcessing() {
if (!myProcessingEnabled.getAndSet(true)) {
super.processQueue();
}
}
@Override
protected void processQueue() {
if (myProcessingEnabled.get()) {
super.processQueue();
}
}
}
private class ConstantSearch implements Callbacks.ConstantAffectionResolver {
private ConstantSearch() {
}
@Nullable @Override
public Future<Callbacks.ConstantAffection> request(String ownerClassName, String fieldName, int accessFlags, boolean fieldRemoved, boolean accessChanged) {
final CmdlineRemoteProto.Message.BuilderMessage.ConstantSearchTask.Builder task =
CmdlineRemoteProto.Message.BuilderMessage.ConstantSearchTask.newBuilder();
task.setOwnerClassName(ownerClassName);
task.setFieldName(fieldName);
task.setAccessFlags(accessFlags);
task.setIsAccessChanged(accessChanged);
task.setIsFieldRemoved(fieldRemoved);
final ConstantSearchFuture future = new ConstantSearchFuture(BuildSession.this);
final ConstantSearchFuture prev = mySearchTasks.put(Pair.create(ownerClassName, fieldName), future);
if (prev != null) {
prev.setDone();
}
myChannel.writeAndFlush(CmdlineProtoUtil.toMessage(mySessionId, CmdlineRemoteProto.Message.BuilderMessage.newBuilder()
.setType(CmdlineRemoteProto.Message.BuilderMessage.Type.CONSTANT_SEARCH_TASK).setConstantSearchTask(task.build()).build()));
return future;
}
}
private static class ConstantSearchFuture extends BasicFuture<Callbacks.ConstantAffection> {
private volatile Callbacks.ConstantAffection myResult = Callbacks.ConstantAffection.EMPTY;
private final CanceledStatus myCanceledStatus;
private ConstantSearchFuture(CanceledStatus canceledStatus) {
myCanceledStatus = canceledStatus;
}
public void setResult(final Collection<File> affectedFiles) {
myResult = new Callbacks.ConstantAffection(affectedFiles);
setDone();
}
@Override
public Callbacks.ConstantAffection get() throws InterruptedException, ExecutionException {
while (true) {
try {
return get(300L, TimeUnit.MILLISECONDS);
}
catch (TimeoutException ignored) {
}
if (myCanceledStatus.isCanceled()) {
return myResult;
}
}
}
@Override
public Callbacks.ConstantAffection get(long timeout, TimeUnit unit) throws InterruptedException, ExecutionException, TimeoutException {
super.get(timeout, unit);
return myResult;
}
}
}