com.google.gapid.server.GapisProcess.java Source code

Java tutorial

Introduction

Here is the source code for com.google.gapid.server.GapisProcess.java

Source

/*
 * Copyright (C) 2017 Google 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.google.gapid.server;

import static com.google.gapid.util.Logging.logLevel;
import static java.util.logging.Level.INFO;
import static java.util.logging.Level.WARNING;

import com.google.common.collect.Lists;
import com.google.common.util.concurrent.Futures;
import com.google.common.util.concurrent.ListenableFuture;
import com.google.gapid.util.Logging;

import java.io.File;
import java.security.SecureRandom;
import java.util.Base64;
import java.util.List;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
import java.util.logging.Logger;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

/**
 * {@link ChildProcess} running the Graphics API Server (GAPIS) executable. The result of the
 * future returned by {@link #start()} is the port the GAPIS server listens to.
 */
public class GapisProcess extends ChildProcess<Integer> {
    private static final Logger LOG = Logger.getLogger(GapisProcess.class.getName());

    private static final Pattern PORT_PATTERN = Pattern.compile("^Bound on port '(\\d+)'$", 0);

    /** The length in characters of an auth-token */
    private static final int AUTH_TOKEN_LENGTH = 8;

    private static final int SERVER_LAUNCH_TIMEOUT_MS = 10000;
    private static final String SERVER_HOST = "localhost";

    private final ListenableFuture<GapisConnection> connection;
    private final String authToken = generateAuthToken();
    private final PanicDetector panicDetector = new PanicDetector();
    private final Listener listener;

    public GapisProcess(Listener listener) {
        super("gapis");
        this.listener = (listener == null) ? Listener.NULL : listener;
        connection = Futures.transform(start(), port -> {
            LOG.log(INFO, "Established a new client connection to " + port);
            return GapisConnection.create(SERVER_HOST + ":" + port, authToken, con -> {
                shutdown();
            });
        });
    }

    @Override
    protected Exception prepare(ProcessBuilder pb) {
        if (!GapiPaths.isValid()) {
            LOG.log(WARNING, "Could not find gapis, but needed to start the server.");
            return new Exception("Could not find the gapis executable.");
        }

        List<String> args = Lists.newArrayList();
        args.add(GapiPaths.gapis().getAbsolutePath());

        File logDir = Logging.getLogDir();
        if (logDir != null) {
            args.add("-log-file");
            args.add(new File(logDir, "gapis.log").getAbsolutePath());
            args.add("-log-level");
            args.add(logLevel.get().gapisLevel);
            args.add("-gapir-args");
            args.add("--log " + new File(logDir, "gapir.log").getAbsolutePath() + " --log-level "
                    + logLevel.get().gapirLevel);
        }

        File strings = GapiPaths.strings();
        if (strings.exists()) {
            args.add("--strings");
            args.add(strings.getAbsolutePath());
        }

        args.add("--gapis-auth-token");
        args.add(authToken);

        pb.command(args);
        return null;
    }

    @Override
    protected OutputHandler<Integer> createStdoutHandler() {
        return new LoggingStringHandler<Integer>(LOG, name, false, line -> {
            panicDetector.processLine(line);
            if (!connection.isDone()) {
                Matcher matcher = PORT_PATTERN.matcher(line);
                if (matcher.matches()) {
                    int port = Integer.parseInt(matcher.group(1));
                    LOG.log(INFO, "Detected gapis startup on port " + port);
                    return port;
                }
            }
            return null;
        });
    }

    @Override
    protected OutputHandler<Integer> createStderrHandler() {
        return new LoggingStringHandler<Integer>(LOG, name, true, line -> {
            panicDetector.processLine(line);
            return null;
        });
    }

    @Override
    protected void onExit(int code) {
        super.onExit(code);
        listener.onServerExit(code, panicDetector.hasFoundPanic() ? panicDetector.getPanic() : null);
    }

    public GapisConnection connect() {
        try {
            return connection.get(SERVER_LAUNCH_TIMEOUT_MS, TimeUnit.MILLISECONDS);
        } catch (InterruptedException e) {
            LOG.log(WARNING, "Interrupted while waiting for gapis", e);
        } catch (ExecutionException e) {
            LOG.log(WARNING, "Failed while waiting for gapis", e);
        } catch (TimeoutException e) {
            LOG.log(WARNING, "Timed out waiting for gapis", e);
        }
        return GapisConnection.NOT_CONNECTED;
    }

    /** @return a randomly generated auth-token string. */
    private static String generateAuthToken() {
        SecureRandom rnd = new SecureRandom();
        byte[] bytes = new byte[AUTH_TOKEN_LENGTH * 3 / 4];
        rnd.nextBytes(bytes);
        return Base64.getEncoder().encodeToString(bytes);
    }

    public static interface Listener {
        public static final Listener NULL = new Listener() {
            @Override
            public void onServerExit(int code, String panic) {
                // Do nothing.
            }
        };

        public void onServerExit(int code, String panic);
    }

    private static class PanicDetector {
        private static final int MAX_PANIC_DETAIL_LINES = 256;

        private final StringBuilder panic = new StringBuilder();
        private boolean foundPanic;
        private int count;

        public PanicDetector() {
        }

        public void processLine(String line) {
            if (foundPanic && count < MAX_PANIC_DETAIL_LINES) {
                panic.append(line).append('\n');
                count++;
            } else if (line.startsWith("panic: ") || line.startsWith("fatal error: ")) {
                panic.delete(0, panic.length());
                foundPanic = true;
                count = 0;
                panic.append(line).append('\n');
            }
        }

        public boolean hasFoundPanic() {
            return foundPanic;
        }

        public String getPanic() {
            return panic.toString();
        }
    }
}