/*
* 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.util.HumanReadableException;
import com.google.gson.stream.JsonReader;
import com.google.gson.stream.JsonWriter;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
public class WorkerProcessProtocolZero implements WorkerProcessProtocol {
private static final String TYPE_HANDSHAKE = "handshake";
private static final String TYPE_COMMAND = "command";
private static final String TYPE_RESULT = "result";
private static final String TYPE_ERROR = "error";
private static final String PROTOCOL_VERSION = "0";
private final JsonWriter processStdinWriter;
private final JsonReader processStdoutReader;
private final Path stdErr;
private final Runnable cleanUp;
public WorkerProcessProtocolZero(
JsonWriter processStdinWriter,
JsonReader processStdoutReader,
Path stdErr,
Runnable cleanUp) {
this.processStdinWriter = processStdinWriter;
this.processStdoutReader = processStdoutReader;
this.stdErr = stdErr;
this.cleanUp = cleanUp;
}
/*
Sends a message that looks like this:
[
{
id: <handshakeID>,
type: 'handshake',
protocol_version: '0',
capabilities: []
}
*/
@Override
public void sendHandshake(int handshakeID) throws IOException {
processStdinWriter.beginArray();
processStdinWriter.beginObject();
processStdinWriter.name("id").value(handshakeID);
processStdinWriter.name("type").value(TYPE_HANDSHAKE);
processStdinWriter.name("protocol_version").value(PROTOCOL_VERSION);
processStdinWriter.name("capabilities").beginArray().endArray();
processStdinWriter.endObject();
processStdinWriter.flush();
}
/*
Expects a message that looks like this:
[
{
id: <handshakeID>,
type: 'handshake',
protocol_version: '0',
capabilities: []
}
*/
@Override
public void receiveHandshake(int handshakeID) throws IOException {
int id = -1;
String type = "";
String protocolVersion = "";
try {
processStdoutReader.beginArray();
processStdoutReader.beginObject();
while (processStdoutReader.hasNext()) {
String property = processStdoutReader.nextName();
if (property.equals("id")) {
id = processStdoutReader.nextInt();
} else if (property.equals("type")) {
type = processStdoutReader.nextString();
} else if (property.equals("protocol_version")) {
protocolVersion = processStdoutReader.nextString();
} else if (property.equals("capabilities")) {
try {
processStdoutReader.beginArray();
processStdoutReader.endArray();
} catch (IllegalStateException e) {
throw new HumanReadableException(
"Expected handshake response's \"capabilities\" to " + "be an empty array.");
}
} else {
processStdoutReader.skipValue();
}
}
processStdoutReader.endObject();
} catch (IOException e) {
throw new HumanReadableException(
e,
"Error receiving handshake response from external process.\n"
+ "Stderr from external process:\n%s",
getStdErrorOutput());
}
if (id != handshakeID) {
throw new HumanReadableException(
String.format(
"Expected handshake response's \"id\" value " + "to be \"%d\", got \"%d\" instead.",
handshakeID, id));
}
if (!type.equals(TYPE_HANDSHAKE)) {
throw new HumanReadableException(
String.format(
"Expected handshake response's \"type\" " + "to be \"%s\", got \"%s\" instead.",
TYPE_HANDSHAKE, type));
}
if (!protocolVersion.equals(PROTOCOL_VERSION)) {
throw new HumanReadableException(
String.format(
"Expected handshake response's "
+ "\"protocol_version\" to be \"%s\", got \"%s\" instead.",
PROTOCOL_VERSION, protocolVersion));
}
}
/*
Sends a message that looks like this:
,{
id: <id>,
type: 'command',
args_path: <argsPath>,
stdout_path: <stdoutPath>,
stderr_path: <stderrPath>,
}
*/
@Override
public void sendCommand(int messageID, WorkerProcessCommand command) throws IOException {
processStdinWriter.beginObject();
processStdinWriter.name("id").value(messageID);
processStdinWriter.name("type").value(TYPE_COMMAND);
processStdinWriter.name("args_path").value(command.getArgsPath().toString());
processStdinWriter.name("stdout_path").value(command.getStdOutPath().toString());
processStdinWriter.name("stderr_path").value(command.getStdErrPath().toString());
processStdinWriter.endObject();
processStdinWriter.flush();
}
/*
Expects a message that looks like this:
,{
id: <id>,
type: 'command',
args_path: <argsPath>,
stdout_path: <stdoutPath>,
stderr_path: <stderrPath>,
}
*/
@Override
public WorkerProcessCommand receiveCommand(int messageID) throws IOException {
int id = -1;
String type = "";
String argsPath = "";
String stdoutPath = "";
String stderrPath = "";
try {
processStdoutReader.beginObject();
while (processStdoutReader.hasNext()) {
String property = processStdoutReader.nextName();
if (property.equals("id")) {
id = processStdoutReader.nextInt();
} else if (property.equals("type")) {
type = processStdoutReader.nextString();
} else if (property.equals("args_path")) {
argsPath = processStdoutReader.nextString();
} else if (property.equals("stdout_path")) {
stdoutPath = processStdoutReader.nextString();
} else if (property.equals("stderr_path")) {
stderrPath = processStdoutReader.nextString();
} else {
processStdoutReader.skipValue();
}
}
processStdoutReader.endObject();
} catch (IOException e) {
throw new HumanReadableException(
e,
"Error receiving command from external process.\nStderr from external process:\n%s",
getStdErrorOutput());
}
if (id != messageID) {
throw new HumanReadableException(
String.format(
"Expected command's \"id\" value to be " + "\"%d\", got \"%d\" instead.",
messageID, id));
}
if (!type.equals(TYPE_COMMAND)) {
throw new HumanReadableException(
String.format(
"Expected command's \"type\" " + "to be \"%s\", got \"%s\" instead.",
TYPE_COMMAND, type));
}
return WorkerProcessCommand.of(
Paths.get(argsPath), Paths.get(stdoutPath), Paths.get(stderrPath));
}
/*
Sends a message that looks like this if the job was successful:
,{
id: <messageID>,
type: 'result',
exit_code: 0
}
of a message that looks like this if message was correct but job failed due to various reasons:
,{
id: <messageID>,
type: 'result',
exit_code: <exitCode>
}
or a message that looks like this if process received a message type it cannot
interpret:
,{
id: <messageID>,
type: 'error',
exit_code: 1
}
or a message that looks like this if process received a valid message type but other
attributes of the message were in an inconsistent state:
,{
id: <messageID>,
type: 'error',
exit_code: 2
}
*/
@Override
public void sendCommandResponse(int messageID, String type, int exitCode) throws IOException {
if (!type.equals(TYPE_RESULT) && !type.equals(TYPE_ERROR)) {
throw new HumanReadableException(
String.format(
"Expected response's \"type\" " + "to be one of [\"%s\",\"%s\"], got \"%s\" instead.",
TYPE_RESULT, TYPE_ERROR, type));
}
if (type.equals(TYPE_ERROR) && exitCode != 1 && exitCode != 2) {
throw new HumanReadableException(
String.format(
"For response with type "
+ "\"%s\" exit code is expected to be 1 or 2, got %d instead.",
type, exitCode));
}
processStdinWriter.beginObject();
processStdinWriter.name("id").value(messageID);
processStdinWriter.name("type").value(type);
processStdinWriter.name("exit_code").value(exitCode);
processStdinWriter.endObject();
processStdinWriter.flush();
}
/*
Expects a message that looks like this if the job was successful:
,{
id: <messageID>,
type: 'result',
exit_code: 0
}
of a message that looks like this if message was correct but job failed due to various reasons:
,{
id: <messageID>,
type: 'result',
exit_code: <exitCode>
}
or a message that looks like this if the external tool received a message type it cannot
interpret:
,{
id: <messageID>,
type: 'error',
exit_code: 1
}
or a message that looks like this if the external tool received a valid message type but other
attributes of the message were in an inconsistent state:
,{
id: <messageID>,
type: 'error',
exit_code: 2
}
*/
@Override
public int receiveCommandResponse(int messageID) throws IOException {
int id = -1;
int exitCode = -1;
String type = "";
try {
processStdoutReader.beginObject();
while (processStdoutReader.hasNext()) {
String property = processStdoutReader.nextName();
if (property.equals("id")) {
id = processStdoutReader.nextInt();
} else if (property.equals("type")) {
type = processStdoutReader.nextString();
} else if (property.equals("exit_code")) {
exitCode = processStdoutReader.nextInt();
} else {
processStdoutReader.skipValue();
}
}
processStdoutReader.endObject();
} catch (IOException e) {
throw new HumanReadableException(
e,
"Error receiving command response from external process.\n"
+ "Stderr from external process:\n%s",
getStdErrorOutput());
}
if (id != messageID) {
throw new HumanReadableException(
String.format(
"Expected response's \"id\" value to be " + "\"%d\", got \"%d\" instead.",
messageID, id));
}
if (!type.equals(TYPE_RESULT) && !type.equals(TYPE_ERROR)) {
throw new HumanReadableException(
String.format(
"Expected response's \"type\" " + "to be one of [\"%s\",\"%s\"], got \"%s\" instead.",
TYPE_RESULT, TYPE_ERROR, type));
}
return exitCode;
}
/*
Sends the closing bracket for the JSON array and expects a response containing a closing
bracket as well. Closes the input and output streams and destroys the process.
*/
@Override
public void close() throws IOException {
try {
processStdinWriter.endArray();
processStdinWriter.close();
processStdoutReader.endArray();
processStdoutReader.close();
} finally {
cleanUp.run();
}
}
private String getStdErrorOutput() throws IOException {
StringBuilder sb = new StringBuilder();
try (InputStream inputStream = Files.newInputStream(stdErr)) {
BufferedReader errorReader = new BufferedReader(new InputStreamReader(inputStream));
while (errorReader.ready()) {
sb.append("\t").append(errorReader.readLine()).append("\n");
}
}
return sb.toString();
}
}