/*
* Copyright 2016-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.shell;
import com.facebook.buck.event.ConsoleEvent;
import com.facebook.buck.step.ExecutionContext;
import com.facebook.buck.step.Step;
import com.facebook.buck.step.StepExecutionResult;
import com.facebook.buck.util.Escaper;
import com.facebook.buck.util.HumanReadableException;
import com.facebook.buck.util.Verbosity;
import com.facebook.buck.util.environment.Platform;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.base.Joiner;
import com.google.common.collect.FluentIterable;
import com.google.common.collect.ImmutableMap;
import java.util.Map;
import java.util.Optional;
public class WorkerShellStep implements Step {
private Optional<WorkerJobParams> cmdParams;
private Optional<WorkerJobParams> bashParams;
private Optional<WorkerJobParams> cmdExeParams;
private WorkerProcessPoolFactory factory;
/**
* Creates new shell step that uses worker process to delegate work. If platform-specific params
* are present they are used in favor of universal params.
*
* @param cmdParams Universal, platform independent params, something that would work for both
* Linux/macOS and Windows platforms.
* @param bashParams Used in Linux/macOS environment, specifies the arguments that are passed into
* bash shell.
* @param cmdExeParams Used in Windows environment, specifies the arguments that are passed into
* cmd.exe (Windows shell).
*/
public WorkerShellStep(
Optional<WorkerJobParams> cmdParams,
Optional<WorkerJobParams> bashParams,
Optional<WorkerJobParams> cmdExeParams,
WorkerProcessPoolFactory factory) {
this.cmdParams = cmdParams;
this.bashParams = bashParams;
this.cmdExeParams = cmdExeParams;
this.factory = factory;
}
@Override
public StepExecutionResult execute(final ExecutionContext context) throws InterruptedException {
WorkerProcessPool pool = null;
WorkerProcess process = null;
try {
// Use the process's startup command as the key.
WorkerJobParams paramsToUse = getWorkerJobParamsToUse(context.getPlatform());
pool = factory.getWorkerProcessPool(context, paramsToUse.getWorkerProcessParams());
process = pool.borrowWorkerProcess();
WorkerJobResult result = process.submitAndWaitForJob(getExpandedJobArgs(context));
pool.returnWorkerProcess(process);
process = null; // to avoid finally below
Verbosity verbosity = context.getVerbosity();
if (result.getStdout().isPresent()
&& !result.getStdout().get().isEmpty()
&& verbosity.shouldPrintOutput()) {
context.postEvent(ConsoleEvent.info("%s", result.getStdout().get()));
}
if (result.getStderr().isPresent()
&& !result.getStderr().get().isEmpty()
&& verbosity.shouldPrintStandardInformation()) {
if (result.getExitCode() == 0) {
context.postEvent(ConsoleEvent.warning("%s", result.getStderr().get()));
} else {
context.postEvent(ConsoleEvent.severe("%s", result.getStderr().get()));
}
}
return StepExecutionResult.of(result.getExitCode());
} catch (Exception e) {
throw new HumanReadableException(e, "Error communicating with external process.");
} finally {
if (pool != null && process != null) {
pool.destroyWorkerProcess(process);
}
}
}
@VisibleForTesting
String getExpandedJobArgs(ExecutionContext context) {
return expandEnvironmentVariables(
this.getWorkerJobParamsToUse(context.getPlatform()).getJobArgs(),
getEnvironmentVariables(context));
}
@VisibleForTesting
String expandEnvironmentVariables(String string, ImmutableMap<String, String> variablesToExpand) {
for (Map.Entry<String, String> variable : variablesToExpand.entrySet()) {
string =
string
.replace("$" + variable.getKey(), variable.getValue())
.replace("${" + variable.getKey() + "}", variable.getValue());
}
return string;
}
@VisibleForTesting
public WorkerJobParams getWorkerJobParamsToUse(Platform platform) {
if (platform == Platform.WINDOWS) {
if (cmdExeParams.isPresent()) {
return cmdExeParams.get();
} else if (cmdParams.isPresent()) {
return cmdParams.get();
} else {
throw new HumanReadableException(
"You must specify either \"cmd_exe\" or \"cmd\" for " + "this build rule.");
}
} else {
if (bashParams.isPresent()) {
return bashParams.get();
} else if (cmdParams.isPresent()) {
return cmdParams.get();
} else {
throw new HumanReadableException(
"You must specify either \"bash\" or \"cmd\" for " + "this build rule.");
}
}
}
/**
* Returns the environment variables to use when expanding the job arguments that get sent to the
* process.
*
* <p>By default, this method returns an empty map.
*
* @param context that may be useful when determining environment variables to include.
*/
protected ImmutableMap<String, String> getEnvironmentVariables(ExecutionContext context) {
return ImmutableMap.of();
}
@Override
public String getShortName() {
return "worker";
}
@VisibleForTesting
WorkerProcessPoolFactory getFactory() {
return factory;
}
@Override
public final String getDescription(ExecutionContext context) {
return String.format(
"Sending job with args \'%s\' to the process started with \'%s\'",
getExpandedJobArgs(context),
FluentIterable.from(
factory.getCommand(
context.getPlatform(),
getWorkerJobParamsToUse(context.getPlatform()).getWorkerProcessParams()))
.transform(Escaper.SHELL_ESCAPER)
.join(Joiner.on(' ')));
}
}