/*
* Copyright 2015-present Facebook, 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.
*/
package com.facebook.buck.intellij.ideabuck.build;
import com.facebook.buck.intellij.ideabuck.config.BuckModule;
import com.facebook.buck.intellij.ideabuck.config.BuckSettingsProvider;
import com.facebook.buck.intellij.ideabuck.ui.BuckEventsConsumer;
import com.intellij.execution.ExecutionException;
import com.intellij.execution.configurations.GeneralCommandLine;
import com.intellij.execution.process.OSProcessHandler;
import com.intellij.execution.process.ProcessEvent;
import com.intellij.execution.process.ProcessListener;
import com.intellij.execution.process.ProcessOutputTypes;
import com.intellij.openapi.diagnostic.Logger;
import com.intellij.openapi.progress.ProcessCanceledException;
import com.intellij.openapi.project.Project;
import com.intellij.openapi.util.Key;
import com.intellij.openapi.vcs.LineHandlerHelper;
import com.intellij.openapi.vfs.CharsetToolkit;
import java.io.File;
import java.nio.charset.Charset;
import java.util.regex.Pattern;
import org.jetbrains.annotations.Nullable;
/** The handler for buck commands with text outputs. */
public abstract class BuckCommandHandler {
protected static final Logger LOG = Logger.getInstance(BuckCommandHandler.class);
private static final long LONG_TIME = 10 * 1000;
protected final Project project;
protected final BuckCommand command;
private final File workingDirectory;
private final GeneralCommandLine commandLine;
private final Object processStateLock = new Object();
private static final Pattern CHARACTER_DIGITS_PATTERN = Pattern.compile("(?s).*[A-Z0-9a-z]+.*");
private final boolean doStartNotify;
@SuppressWarnings({"FieldAccessedSynchronizedAndUnsynchronized"})
private Process process;
@SuppressWarnings({"FieldAccessedSynchronizedAndUnsynchronized"})
private OSProcessHandler handler;
/** Character set to use for IO. */
@SuppressWarnings({"FieldAccessedSynchronizedAndUnsynchronized"})
private Charset charset = CharsetToolkit.UTF8_CHARSET;
/** Buck execution start timestamp. */
private long startTime;
/** The partial line from stderr stream. */
private final StringBuilder stderrLine = new StringBuilder();
public BuckCommandHandler(Project project, File directory, BuckCommand command) {
this(project, directory, command, /* doStartNotify */ false);
}
/**
* @param project a project
* @param directory a process directory
* @param command a command to execute (if empty string, the parameter is ignored)
* @param doStartNotify true if the handler should call OSHandler#startNotify
*/
public BuckCommandHandler(
Project project, File directory, BuckCommand command, boolean doStartNotify) {
this.doStartNotify = doStartNotify;
String buckExecutable = BuckSettingsProvider.getInstance().getState().buckExecutable;
this.project = project;
this.command = command;
commandLine = new GeneralCommandLine();
commandLine.setExePath(buckExecutable);
workingDirectory = directory;
commandLine.withWorkDirectory(workingDirectory);
commandLine.addParameter(command.name());
for (String parameter : command.getParameters()) {
commandLine.addParameter(parameter);
}
}
/** Start process */
public synchronized void start() {
checkNotStarted();
try {
startTime = System.currentTimeMillis();
process = startProcess();
startHandlingStreams();
} catch (ProcessCanceledException e) {
LOG.warn(e);
} catch (Throwable t) {
if (!project.isDisposed()) {
LOG.error(t);
}
}
}
/** Stop process */
public synchronized void stop() {
process.destroy();
}
/** @return true if process is started. */
public final synchronized boolean isStarted() {
return process != null;
}
/**
* Check that process is not started yet.
*
* @throws IllegalStateException if process has been already started
*/
private void checkNotStarted() {
if (isStarted()) {
throw new IllegalStateException("The process has been already started");
}
}
/**
* Check that process is started.
*
* @throws IllegalStateException if process has not been started
*/
protected final void checkStarted() {
if (!isStarted()) {
throw new IllegalStateException("The process is not started yet");
}
}
public GeneralCommandLine command() {
return commandLine;
}
/** @return a context project */
public Project project() {
return project;
}
/** Start the buck process. */
@Nullable
protected Process startProcess() throws ExecutionException {
synchronized (processStateLock) {
handler = createProcess(commandLine);
return handler.getProcess();
}
}
/** Start handling process output streams for the handler. */
protected void startHandlingStreams() {
if (handler == null) {
return;
}
handler.addProcessListener(
new ProcessListener() {
public void startNotified(final ProcessEvent event) {}
public void processTerminated(final ProcessEvent event) {
BuckCommandHandler.this.processTerminated();
}
public void processWillTerminate(
final ProcessEvent event, final boolean willBeDestroyed) {}
public void onTextAvailable(final ProcessEvent event, final Key outputType) {
BuckCommandHandler.this.onTextAvailable(event.getText(), outputType);
}
});
if (doStartNotify) {
handler.startNotify();
}
}
protected boolean processExitSuccesfull() {
return process.exitValue() == 0;
}
/** Wait for process termination. */
public void waitFor() {
checkStarted();
if (handler != null) {
// handler.waitFor will wait for a semaphore which will be released when the started
// process terminates if doStartNotify is true.
// If the following call never returns, please check the value of doStartNotify
handler.waitFor();
}
}
public OSProcessHandler createProcess(GeneralCommandLine commandLine) throws ExecutionException {
// TODO(t7984081): Use ProcessExecutor to start buck process.
Process process = commandLine.createProcess();
return new OSProcessHandler(process, commandLine.getCommandLineString(), charset);
}
public void runInCurrentThread(@Nullable Runnable postStartAction) {
if (!beforeCommand()) {
return;
}
start();
if (isStarted()) {
if (postStartAction != null) {
postStartAction.run();
}
waitFor();
}
afterCommand();
logTime();
}
private void logTime() {
if (startTime > 0) {
long time = System.currentTimeMillis() - startTime;
if (!LOG.isDebugEnabled() && time > LONG_TIME) {
LOG.info(
String.format(
"buck %s took %s ms. Command parameters: %n%s",
command, time, commandLine.getCommandLineString()));
} else {
LOG.debug(String.format("buck %s took %s ms", command, time));
}
} else {
LOG.debug(String.format("buck %s finished.", command));
}
}
protected void processTerminated() {
if (stderrLine.length() != 0) {
onTextAvailable("\n", ProcessOutputTypes.STDERR);
}
}
protected void onTextAvailable(final String text, final Key outputType) {
notifyLines(outputType, LineHandlerHelper.splitText(text));
}
/**
* Notify listeners for each complete line. Note that in the case of stderr, the last line is
* saved.
*/
protected void notifyLines(final Key outputType, final Iterable<String> lines) {
BuckEventsConsumer buckEventsConsumer =
project.getComponent(BuckModule.class).getBuckEventsConsumer();
if (outputType == ProcessOutputTypes.STDERR) {
StringBuilder stderr = new StringBuilder();
for (String line : lines) {
// Check if the line has at least one character or digit
if (CHARACTER_DIGITS_PATTERN.matcher(line).matches()) {
stderr.append(line);
}
}
if (stderr.length() != 0) {
buckEventsConsumer.consumeConsoleEvent(stderr.toString());
}
}
}
protected abstract boolean beforeCommand();
protected abstract void afterCommand();
public OSProcessHandler getHandler() {
return handler;
}
}