| // Copyright 2016 The Bazel Authors. All rights reserved. | |
| // | |
| // 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.google.devtools.build.lib.server; | |
| import static java.nio.charset.StandardCharsets.ISO_8859_1; | |
| import com.google.common.annotations.VisibleForTesting; | |
| import com.google.common.base.Strings; | |
| import com.google.common.collect.ImmutableList; | |
| import com.google.common.flogger.GoogleLogger; | |
| import com.google.devtools.build.lib.bugreport.BugReport; | |
| import com.google.devtools.build.lib.clock.Clock; | |
| import com.google.devtools.build.lib.runtime.BlazeCommandResult; | |
| import com.google.devtools.build.lib.runtime.CommandDispatcher; | |
| import com.google.devtools.build.lib.runtime.CommandDispatcher.UiVerbosity; | |
| import com.google.devtools.build.lib.runtime.SafeRequestLogging; | |
| import com.google.devtools.build.lib.runtime.proto.InvocationPolicyOuterClass.InvocationPolicy; | |
| import com.google.devtools.build.lib.server.CommandManager.RunningCommand; | |
| import com.google.devtools.build.lib.server.CommandProtos.CancelRequest; | |
| import com.google.devtools.build.lib.server.CommandProtos.CancelResponse; | |
| import com.google.devtools.build.lib.server.CommandProtos.PingRequest; | |
| import com.google.devtools.build.lib.server.CommandProtos.PingResponse; | |
| import com.google.devtools.build.lib.server.CommandProtos.RunRequest; | |
| import com.google.devtools.build.lib.server.CommandProtos.RunResponse; | |
| import com.google.devtools.build.lib.server.CommandProtos.ServerInfo; | |
| import com.google.devtools.build.lib.server.CommandProtos.StartupOption; | |
| import com.google.devtools.build.lib.server.CommandProtos.TerminalSizeRequest; | |
| import com.google.devtools.build.lib.server.CommandProtos.TerminalSizeResponse; | |
| import com.google.devtools.build.lib.server.FailureDetails.Command; | |
| import com.google.devtools.build.lib.server.FailureDetails.FailureDetail; | |
| import com.google.devtools.build.lib.server.FailureDetails.Filesystem; | |
| import com.google.devtools.build.lib.server.FailureDetails.Filesystem.Code; | |
| import com.google.devtools.build.lib.server.FailureDetails.GrpcServer; | |
| import com.google.devtools.build.lib.util.AbruptExitException; | |
| import com.google.devtools.build.lib.util.DetailedExitCode; | |
| import com.google.devtools.build.lib.util.ExitCode; | |
| import com.google.devtools.build.lib.util.InterruptedFailureDetails; | |
| import com.google.devtools.build.lib.util.Pair; | |
| import com.google.devtools.build.lib.util.io.CommandExtensionReporter; | |
| import com.google.devtools.build.lib.util.io.OutErr; | |
| import com.google.devtools.build.lib.vfs.FileSystemUtils; | |
| import com.google.devtools.build.lib.vfs.Path; | |
| import com.google.devtools.common.options.InvocationPolicyParser; | |
| import com.google.devtools.common.options.OptionsParsingException; | |
| import com.google.errorprone.annotations.CanIgnoreReturnValue; | |
| import com.google.protobuf.Any; | |
| import com.google.protobuf.ByteString; | |
| import com.google.protobuf.ExtensionRegistry; | |
| import com.google.protobuf.InvalidProtocolBufferException; | |
| import java.io.IOException; | |
| import java.io.OutputStream; | |
| import java.net.Inet4Address; | |
| import java.net.Inet6Address; | |
| import java.net.InetSocketAddress; | |
| import java.net.SocketAddress; | |
| import java.nio.charset.StandardCharsets; | |
| import java.security.MessageDigest; | |
| import java.security.SecureRandom; | |
| import java.time.Duration; | |
| import java.util.Optional; | |
| import javax.annotation.Nullable; | |
| /** | |
| * Manages the client request/response loop. | |
| * | |
| * <p>In addition to the request threads (managed by {@link GrpcCommandServer}), we maintain one | |
| * extra thread for handling the server timeout, and an interrupt watcher thread is started for each | |
| * interrupt request that logs if it takes too long to take effect. | |
| * | |
| * <p>Each running RPC has a UUID associated with it that is used to identify it when a client wants | |
| * to cancel it. Cancellation is done by the client sending the server a Cancel RPC, which results | |
| * in the main thread of the command being interrupted. A client going away without sending one has | |
| * the same effect, see {@link GrpcCommandServerImpl.BlockingStreamObserver}. | |
| */ | |
| public class CommandServer implements GrpcCommandServer.Callback { | |
| private static final GoogleLogger logger = GoogleLogger.forEnclosingClass(); | |
| public static CommandServer create( | |
| GrpcCommandServer grpcCommandServer, | |
| CommandDispatcher dispatcher, | |
| ShutdownHooks shutdownHooks, | |
| PidFileWatcher pidFileWatcher, | |
| Clock clock, | |
| int port, | |
| Path serverDirectory, | |
| int serverPid, | |
| int maxIdleSeconds, | |
| boolean shutdownOnLowSysMem, | |
| boolean idleServerTasks, | |
| @Nullable String slowInterruptMessageSuffix) { | |
| SecureRandom random = new SecureRandom(); | |
| return new CommandServer( | |
| grpcCommandServer, | |
| dispatcher, | |
| shutdownHooks, | |
| pidFileWatcher, | |
| clock, | |
| port, | |
| generateCookie(random, 16), | |
| generateCookie(random, 16), | |
| serverDirectory, | |
| serverPid, | |
| maxIdleSeconds, | |
| shutdownOnLowSysMem, | |
| idleServerTasks, | |
| slowInterruptMessageSuffix); | |
| } | |
| @VisibleForTesting | |
| enum StreamType { | |
| STDOUT, | |
| STDERR, | |
| } | |
| /** Command extension reporter that packs the protobuf into a RunResponse and sends it. */ | |
| private static class RpcCommandExtensionReporter implements CommandExtensionReporter { | |
| // Store commandId and responseCookie as ByteStrings to avoid String -> UTF8 bytes conversion | |
| // for each serialized chunk of output. | |
| private final ByteString commandIdBytes; | |
| private final ByteString responseCookieBytes; | |
| private final GrpcCommandServer.Responder responder; | |
| RpcCommandExtensionReporter( | |
| String commandId, String responseCookie, GrpcCommandServer.Responder responder) { | |
| this.commandIdBytes = ByteString.copyFromUtf8(commandId); | |
| this.responseCookieBytes = ByteString.copyFromUtf8(responseCookie); | |
| this.responder = responder; | |
| } | |
| @Override | |
| public synchronized void report(Any commandExtension) throws IOException { | |
| responder.onNext( | |
| RunResponse.newBuilder() | |
| .setCookieBytes(responseCookieBytes) | |
| .setCommandIdBytes(commandIdBytes) | |
| .setStandardOutput(ByteString.EMPTY) | |
| .addCommandExtensions(commandExtension) | |
| .build() | |
| .toByteArray()); | |
| } | |
| } | |
| /** | |
| * An output stream that forwards the data written to it over the gRPC command stream. | |
| * | |
| * <p>Note that wraping this class with a {@code Channel} can cause a deadlock if there is an | |
| * {@link OutputStream} in between that synchronizes both on {@code #close()} and {@code #write()} | |
| * because then if an interrupt happens in {@code FlowControl#onNext}, the thread on which {@code | |
| * interrupt()} was called will wait until the {@code Channel} closes itself while holding a lock | |
| * for interrupting the thread on which {@code FlowControl#onNext} is being executed and that | |
| * thread will hold a lock that is needed for the {@code Channel} to be closed and call {@code | |
| * interrupt()} in {@code FlowControl#onNext}, which will in turn try to acquire the interrupt | |
| * lock. | |
| */ | |
| private static class RpcOutputStream extends OutputStream { | |
| private static final int CHUNK_SIZE = 8192; | |
| // Store commandId and responseCookie as ByteStrings to avoid String -> UTF8 bytes conversion | |
| // for each serialized chunk of output. | |
| private final ByteString commandIdBytes; | |
| private final ByteString responseCookieBytes; | |
| private final StreamType type; | |
| private final GrpcCommandServer.Responder responder; | |
| RpcOutputStream( | |
| String commandId, | |
| String responseCookie, | |
| StreamType type, | |
| GrpcCommandServer.Responder responder) { | |
| this.commandIdBytes = ByteString.copyFromUtf8(commandId); | |
| this.responseCookieBytes = ByteString.copyFromUtf8(responseCookie); | |
| this.type = type; | |
| this.responder = responder; | |
| } | |
| @Override | |
| public void write(byte[] b, int off, int inlen) throws IOException { | |
| for (int i = 0; i < inlen; i += CHUNK_SIZE) { | |
| ByteString input = ByteString.copyFrom(b, off + i, Math.min(CHUNK_SIZE, inlen - i)); | |
| RunResponse.Builder response = | |
| RunResponse.newBuilder() | |
| .setCookieBytes(responseCookieBytes) | |
| .setCommandIdBytes(commandIdBytes); | |
| switch (type) { | |
| case STDOUT -> response.setStandardOutput(input); | |
| case STDERR -> response.setStandardError(input); | |
| } | |
| try { | |
| // This can block waiting for the client to read the available data. | |
| responder.onNext(response.build().toByteArray()); | |
| } catch (IOException e) { | |
| // I am not sure whether there are any circumstances under which this call could throw an | |
| // exception, but I'd rather it be logged than that we crash silently. The documentation | |
| // only says that onNext does not throw a CancelledException if the stream is canceled, | |
| // but otherwise does not say anything about exceptions that can be thrown from onNext. | |
| // Note that Blaze redirects System.{out,err} to this output stream, so attempting to call | |
| // printStackTrace() from here could go into an infinite loop. | |
| BugReport.sendBugReport(e); | |
| Thread.currentThread().interrupt(); | |
| } | |
| } | |
| } | |
| @Override | |
| public void write(int byteAsInt) throws IOException { | |
| write(new byte[] {(byte) byteAsInt}, 0, 1); | |
| } | |
| } | |
| // These paths are all relative to the server directory | |
| private static final String PORT_FILE = "command_port"; | |
| private static final String REQUEST_COOKIE_FILE = "request_cookie"; | |
| private static final String RESPONSE_COOKIE_FILE = "response_cookie"; | |
| private static final String SERVER_INFO_FILE = "server_info.rawproto"; | |
| private final GrpcCommandServer grpcCommandServer; | |
| private final CommandManager commandManager; | |
| private final CommandDispatcher dispatcher; | |
| private final ShutdownHooks shutdownHooks; | |
| private final Clock clock; | |
| private final Path serverDirectory; | |
| private final String requestCookie; | |
| private final String responseCookie; | |
| private final int maxIdleSeconds; | |
| private final boolean shutdownOnLowSysMem; | |
| private final PidFileWatcher pidFileWatcher; | |
| private final int serverPid; | |
| private final int port; | |
| @VisibleForTesting | |
| CommandServer( | |
| GrpcCommandServer grpcCommandServer, | |
| CommandDispatcher dispatcher, | |
| ShutdownHooks shutdownHooks, | |
| PidFileWatcher pidFileWatcher, | |
| Clock clock, | |
| int port, | |
| String requestCookie, | |
| String responseCookie, | |
| Path serverDirectory, | |
| int serverPid, | |
| int maxIdleSeconds, | |
| boolean shutdownOnLowSysMem, | |
| boolean doIdleServerTasks, | |
| @Nullable String slowInterruptMessageSuffix) { | |
| this.grpcCommandServer = grpcCommandServer; | |
| this.dispatcher = dispatcher; | |
| this.shutdownHooks = shutdownHooks; | |
| this.pidFileWatcher = pidFileWatcher; | |
| this.clock = clock; | |
| this.port = port; | |
| this.requestCookie = requestCookie; | |
| this.responseCookie = responseCookie; | |
| this.serverDirectory = serverDirectory; | |
| this.serverPid = serverPid; | |
| this.maxIdleSeconds = maxIdleSeconds; | |
| this.shutdownOnLowSysMem = shutdownOnLowSysMem; | |
| commandManager = new CommandManager(doIdleServerTasks, slowInterruptMessageSuffix); | |
| } | |
| private static String generateCookie(SecureRandom random, int byteCount) { | |
| byte[] bytes = new byte[byteCount]; | |
| random.nextBytes(bytes); | |
| StringBuilder result = new StringBuilder(); | |
| for (byte b : bytes) { | |
| result.append(Integer.toHexString(b + 128)); | |
| } | |
| return result.toString(); | |
| } | |
| /** | |
| * This is called when the server is shut down as a result of a "clean --expunge". | |
| * | |
| * <p>In this case, no files should be deleted on shutdown hooks, since clean also deletes the | |
| * lock file, and there is a small possibility of the following sequence of events: | |
| * | |
| * <ol> | |
| * <li>Client 1 runs "blaze clean --expunge" | |
| * <li>Client 2 runs a command and waits for client 1 to finish | |
| * <li>The clean command deletes everything including the lock file | |
| * <li>Client 2 starts running and since the output base is empty, starts up a new server, which | |
| * creates its own socket and PID files | |
| * <li>The server used by client runs its shutdown hooks, deleting the PID files created by the | |
| * new server | |
| * </ol> | |
| * | |
| * It also disables the "die when the PID file changes" handler so that it doesn't kill the server | |
| * while the "clean --expunge" command is running. | |
| */ | |
| public void prepareForAbruptShutdown() { | |
| shutdownHooks.disable(); | |
| pidFileWatcher.signalShutdown(); | |
| } | |
| /** Interrupts (cancels) in-flight commands. */ | |
| public void interrupt() { | |
| commandManager.interruptInflightCommands(); | |
| } | |
| /** | |
| * Starts serving, writes the server status files, and blocks until the shutdown command is | |
| * received. | |
| */ | |
| public void serveAndAwaitTermination() throws AbruptExitException { | |
| SocketAddress address; | |
| try { | |
| address = serve(); | |
| } catch (IOException e) { | |
| throw new AbruptExitException( | |
| DetailedExitCode.of( | |
| createFailureDetail(e.getMessage(), GrpcServer.Code.SERVER_BIND_FAILURE)), | |
| e); | |
| } | |
| if (maxIdleSeconds > 0) { | |
| Thread timeoutAndMemoryCheckingThread = | |
| new Thread( | |
| new ServerWatcherRunnable( | |
| grpcCommandServer, maxIdleSeconds, shutdownOnLowSysMem, commandManager)); | |
| timeoutAndMemoryCheckingThread.setName("grpc-timeout-and-memory"); | |
| timeoutAndMemoryCheckingThread.setDaemon(true); | |
| timeoutAndMemoryCheckingThread.start(); | |
| } | |
| writeServerStatusFiles(address); | |
| try { | |
| awaitTermination(); | |
| } catch (InterruptedException e) { | |
| // TODO(lberki): Handle SIGINT in a reasonable way | |
| throw new IllegalStateException(e); | |
| } | |
| } | |
| @VisibleForTesting | |
| @CanIgnoreReturnValue | |
| SocketAddress serve() throws IOException { | |
| return grpcCommandServer.serve(port, this); | |
| } | |
| @VisibleForTesting | |
| void shutdown() { | |
| grpcCommandServer.shutdown(); | |
| } | |
| @VisibleForTesting | |
| void shutdownNow() { | |
| grpcCommandServer.shutdownNow(); | |
| } | |
| @VisibleForTesting | |
| void awaitTermination() throws InterruptedException { | |
| grpcCommandServer.awaitTermination(); | |
| } | |
| private void writeServerStatusFiles(SocketAddress address) throws AbruptExitException { | |
| String addressString = formatAddress(address); | |
| writeServerFile(PORT_FILE, addressString); | |
| writeServerFile(REQUEST_COOKIE_FILE, requestCookie); | |
| writeServerFile(RESPONSE_COOKIE_FILE, responseCookie); | |
| ServerInfo info = | |
| ServerInfo.newBuilder() | |
| .setPid(serverPid) | |
| .setAddress(addressString) | |
| .setRequestCookie(requestCookie) | |
| .setResponseCookie(responseCookie) | |
| .build(); | |
| // Write then mv so the user never sees incomplete contents. | |
| Path serverInfoTmpFile = serverDirectory.getChild(SERVER_INFO_FILE + ".tmp"); | |
| try { | |
| try (OutputStream out = serverInfoTmpFile.getOutputStream()) { | |
| info.writeTo(out); | |
| } | |
| Path serverInfoFile = serverDirectory.getChild(SERVER_INFO_FILE); | |
| serverInfoTmpFile.renameTo(serverInfoFile); | |
| shutdownHooks.deleteAtExit(serverInfoFile); | |
| } catch (IOException e) { | |
| throw createFilesystemFailureException("Failed to write server info file", e); | |
| } | |
| } | |
| private String formatAddress(SocketAddress address) { | |
| if (address instanceof InetSocketAddress inetAddress) { | |
| if (inetAddress.getAddress() instanceof Inet4Address inet4Addr) { | |
| return inet4Addr.getHostAddress() + ":" + inetAddress.getPort(); | |
| } else if (inetAddress.getAddress() instanceof Inet6Address inet6Addr) { | |
| return "[" + inet6Addr.getHostAddress() + "]:" + inetAddress.getPort(); | |
| } | |
| } | |
| // Can only happen in tests using an in-memory implementation; representation doesn't matter. | |
| return address.toString(); | |
| } | |
| private void writeServerFile(String name, String contents) throws AbruptExitException { | |
| Path file = serverDirectory.getChild(name); | |
| try { | |
| FileSystemUtils.writeContentAsLatin1(file, contents); | |
| } catch (IOException e) { | |
| throw createFilesystemFailureException("Server file (" + file + ") write failed", e); | |
| } | |
| shutdownHooks.deleteAtExit(file); | |
| } | |
| @Override | |
| public void run(byte[] serializedRequest, GrpcCommandServer.Responder responder) { | |
| RunRequest request; | |
| try { | |
| request = RunRequest.parseFrom(serializedRequest, ExtensionRegistry.getEmptyRegistry()); | |
| } catch (InvalidProtocolBufferException e) { | |
| // Programming error: the SC proto must remain backwards-compatible with the LC proto. | |
| throw new IllegalStateException(e); | |
| } | |
| boolean badCookie = !isValidRequestCookie(request.getCookie()); | |
| if (badCookie || request.getClientDescription().isEmpty()) { | |
| try { | |
| FailureDetail failureDetail = | |
| badCookie | |
| ? createFailureDetail("Invalid RunRequest: bad cookie", GrpcServer.Code.BAD_COOKIE) | |
| : createFailureDetail( | |
| "Invalid RunRequest: no client description", | |
| GrpcServer.Code.NO_CLIENT_DESCRIPTION); | |
| responder.onNext( | |
| RunResponse.newBuilder() | |
| .setFinished(true) | |
| .setExitCode(ExitCode.LOCAL_ENVIRONMENTAL_ERROR.getNumericExitCode()) | |
| .setFailureDetail(failureDetail) | |
| .build() | |
| .toByteArray()); | |
| responder.onCompleted(); | |
| } catch (IOException e) { | |
| logger.atInfo().withCause(e).log("Error while sending RunResponse"); | |
| } | |
| return; | |
| } | |
| String commandId; | |
| BlazeCommandResult result; | |
| // TODO(b/63925394): This information needs to be passed to the GotOptionsEvent, which does not | |
| // currently have the explicit startup options. See Improved Command Line Reporting design doc | |
| // for details. | |
| // Convert the startup options record to Java strings, source first. | |
| ImmutableList.Builder<Pair<String, String>> startupOptions = ImmutableList.builder(); | |
| for (StartupOption option : request.getStartupOptionsList()) { | |
| // UTF-8 won't do because we want to be able to pass arbitrary binary strings. | |
| startupOptions.add( | |
| new Pair<>( | |
| platformBytesToInternalString(option.getSource()), | |
| platformBytesToInternalString(option.getOption()))); | |
| } | |
| commandManager.preemptEligibleCommands(); | |
| try (RunningCommand command = | |
| request.getPreemptible() | |
| ? commandManager.createPreemptibleCommand() | |
| : commandManager.createCommand()) { | |
| commandId = command.getId(); | |
| try { | |
| // Send the client the command id as soon as we know it, letting the responder capture the | |
| // current thread as the one to interrupt if the client goes away. | |
| responder.onNext( | |
| RunResponse.newBuilder() | |
| .setCookie(responseCookie) | |
| .setCommandId(commandId) | |
| .build() | |
| .toByteArray()); | |
| } catch (IOException e) { | |
| logger.atInfo().withCause(e).log("Error while sending initial RunResponse"); | |
| } | |
| OutErr rpcOutErr = | |
| OutErr.create( | |
| new RpcOutputStream(command.getId(), responseCookie, StreamType.STDOUT, responder), | |
| new RpcOutputStream(command.getId(), responseCookie, StreamType.STDERR, responder)); | |
| try { | |
| // Transform args into Bazel's internal string representation. | |
| ImmutableList<String> args = | |
| request.getArgList().stream() | |
| .map(CommandServer::platformBytesToInternalString) | |
| .collect(ImmutableList.toImmutableList()); | |
| InvocationPolicy policy = InvocationPolicyParser.parsePolicy(request.getInvocationPolicy()); | |
| logger.atInfo().log("Executing command %s", SafeRequestLogging.getRequestLogString(args)); | |
| Duration blockForLockTimeout = | |
| request.hasBlockForLockTimeoutMs() | |
| ? Duration.ofMillis(Math.max(0, request.getBlockForLockTimeoutMs())) | |
| : (request.getBlockForLock() ? Duration.ofMillis(Long.MAX_VALUE) : Duration.ZERO); | |
| result = | |
| dispatcher.exec( | |
| policy, | |
| args, | |
| rpcOutErr, | |
| blockForLockTimeout, | |
| request.getQuiet() ? UiVerbosity.QUIET : UiVerbosity.NORMAL, | |
| request.getClientDescription(), | |
| clock.currentTimeMillis(), | |
| Optional.of(startupOptions.build()), | |
| commandManager::getIdleTaskResults, | |
| request.getCommandExtensionsList(), | |
| new RpcCommandExtensionReporter(command.getId(), responseCookie, responder), | |
| command.getTerminalSizeMonitor()); | |
| } catch (OptionsParsingException e) { | |
| rpcOutErr.printErrLn(e.getMessage()); | |
| result = | |
| BlazeCommandResult.detailedExitCode( | |
| DetailedExitCode.of( | |
| FailureDetail.newBuilder() | |
| .setMessage("Invocation policy parsing failed: " + e.getMessage()) | |
| .setCommand( | |
| Command.newBuilder() | |
| .setCode(Command.Code.INVOCATION_POLICY_PARSE_FAILURE)) | |
| .build())); | |
| } | |
| // Record tasks to be run by IdleTaskManager. This is triggered in RunningCommand#close() | |
| // (as a Closeable), as we go out of scope immediately after this. | |
| command.setIdleTasks(result.getIdleTasks()); | |
| } catch (InterruptedException e) { | |
| result = | |
| BlazeCommandResult.detailedExitCode( | |
| InterruptedFailureDetails.detailedExitCode("Command dispatch interrupted")); | |
| commandId = ""; // The default value, the client will ignore it | |
| } | |
| RunResponse.Builder response = | |
| RunResponse.newBuilder() | |
| .setCookie(responseCookie) | |
| .setCommandId(commandId) | |
| .setFinished(true) | |
| .setTerminationExpected(result.shutdown()); | |
| if (result.getExecRequest() != null) { | |
| response.setExitCode(result.getExitCode().getNumericExitCode()); | |
| response.setExecRequest(result.getExecRequest()); | |
| if (result.getFailureDetail() != null) { | |
| response.setFailureDetail(result.getFailureDetail()); | |
| } | |
| } else { | |
| response.setExitCode(result.getExitCode().getNumericExitCode()); | |
| if (result.getFailureDetail() != null) { | |
| response.setFailureDetail(result.getFailureDetail()); | |
| } | |
| } | |
| try { | |
| responder.onNext( | |
| response.addAllCommandExtensions(result.getResponseExtensions()).build().toByteArray()); | |
| responder.onCompleted(); | |
| } catch (IOException e) { | |
| logger.atInfo().withCause(e).log("Error while sending RunResponse"); | |
| } | |
| if (result.shutdown()) { | |
| grpcCommandServer.shutdown(); | |
| } | |
| } | |
| @Override | |
| public void ping(byte[] serializedRequest, GrpcCommandServer.Responder responder) { | |
| logger.atInfo().log("Got PingRequest"); | |
| PingRequest request; | |
| try { | |
| request = PingRequest.parseFrom(serializedRequest, ExtensionRegistry.getEmptyRegistry()); | |
| } catch (InvalidProtocolBufferException e) { | |
| // Programming error: the SC proto must remain backwards-compatible with the LC proto. | |
| throw new IllegalStateException(e); | |
| } | |
| try (RunningCommand command = commandManager.createCommand()) { | |
| PingResponse.Builder response = PingResponse.newBuilder(); | |
| if (isValidRequestCookie(request.getCookie())) { | |
| response.setCookie(responseCookie); | |
| } | |
| responder.onNext(response.build().toByteArray()); | |
| responder.onCompleted(); | |
| } catch (IOException e) { | |
| // There is no one to report the failure to. | |
| logger.atInfo().withCause(e).log("Error while sending PingResponse"); | |
| } | |
| } | |
| @Override | |
| public void cancel(byte[] serializedRequest, GrpcCommandServer.Responder responder) { | |
| CancelRequest request; | |
| try { | |
| request = CancelRequest.parseFrom(serializedRequest, ExtensionRegistry.getEmptyRegistry()); | |
| } catch (InvalidProtocolBufferException e) { | |
| // Programming error: the SC proto must remain backwards-compatible with the LC proto. | |
| throw new IllegalStateException(e); | |
| } | |
| logger.atInfo().log("Got CancelRequest for command id %s", request.getCommandId()); | |
| try { | |
| if (isValidRequestCookie(request.getCookie())) { | |
| commandManager.doCancel(request); | |
| responder.onNext( | |
| CancelResponse.newBuilder().setCookie(responseCookie).build().toByteArray()); | |
| } | |
| responder.onCompleted(); | |
| } catch (IOException e) { | |
| // There is no one to report the failure to. | |
| logger.atInfo().withCause(e).log("Error while sending CancelResponse"); | |
| } | |
| } | |
| @Override | |
| public void updateTerminalSize(byte[] serializedRequest, GrpcCommandServer.Responder responder) { | |
| TerminalSizeRequest request; | |
| try { | |
| request = | |
| TerminalSizeRequest.parseFrom(serializedRequest, ExtensionRegistry.getEmptyRegistry()); | |
| } catch (InvalidProtocolBufferException e) { | |
| // Programming error: the SC proto must remain backwards-compatible with the LC proto. | |
| throw new IllegalStateException(e); | |
| } | |
| logger.atInfo().log("Got TerminalSizeRequest for command id %s", request.getCommandId()); | |
| try { | |
| if (isValidRequestCookie(request.getCookie())) { | |
| commandManager.doUpdateTerminalSize(request); | |
| responder.onNext( | |
| TerminalSizeResponse.newBuilder().setCookie(responseCookie).build().toByteArray()); | |
| } | |
| responder.onCompleted(); | |
| } catch (IOException e) { | |
| // There is no one to report the failure to. | |
| logger.atInfo().withCause(e).log("Error while sending TerminalSizeResponse"); | |
| } | |
| } | |
| /** | |
| * Returns whether or not the provided cookie is valid for this server using a constant-time | |
| * comparison in order to guard against timing attacks. | |
| */ | |
| private boolean isValidRequestCookie(String incomingRequestCookie) { | |
| // Note that cookie file was written as latin-1, so use that here. | |
| return MessageDigest.isEqual( | |
| incomingRequestCookie.getBytes(StandardCharsets.ISO_8859_1), | |
| requestCookie.getBytes(StandardCharsets.ISO_8859_1)); | |
| } | |
| private static AbruptExitException createFilesystemFailureException( | |
| String message, IOException e) { | |
| return new AbruptExitException( | |
| DetailedExitCode.of( | |
| FailureDetail.newBuilder() | |
| .setMessage( | |
| message + (Strings.isNullOrEmpty(e.getMessage()) ? "" : ": " + e.getMessage())) | |
| .setFilesystem(Filesystem.newBuilder().setCode(Code.SERVER_FILE_WRITE_FAILURE)) | |
| .build()), | |
| e); | |
| } | |
| private static FailureDetail createFailureDetail(String message, GrpcServer.Code detailedCode) { | |
| return FailureDetail.newBuilder() | |
| .setMessage(message) | |
| .setGrpcServer(GrpcServer.newBuilder().setCode(detailedCode)) | |
| .build(); | |
| } | |
| private static String platformBytesToInternalString(ByteString bytes) { | |
| return bytes.toString(ISO_8859_1); | |
| } | |
| } |