aboutsummaryrefslogtreecommitdiff
path: root/remote/server/src/com/android/layoutlib/bridge/remote/server/ServerMain.java
blob: 4c23afc20974680b887963e0db8147770254d511 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
/*
 * Copyright (C) 2017 The Android Open Source Project
 *
 * 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.android.layoutlib.bridge.remote.server;

import com.android.layout.remote.api.RemoteBridge;
import com.android.tools.layoutlib.annotations.NotNull;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.lang.management.ManagementFactory;
import java.lang.management.RuntimeMXBean;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.rmi.NoSuchObjectException;
import java.rmi.RemoteException;
import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;
import java.rmi.server.UnicastRemoteObject;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.TimeUnit;
import java.util.function.Consumer;
import java.util.stream.Collectors;

/**
 * Main server class. The main method will start an RMI server for the {@link RemoteBridgeImpl}
 * class.
 */
public class ServerMain {
    private static final String RUNNING_SERVER_STR = "Server is running on port ";
    public static int REGISTRY_BASE_PORT = 9000;

    private final int mPort;
    private final Registry mRegistry;

    private ServerMain(int port, @NotNull Registry registry) {
        mPort = port;
        mRegistry = registry;
    }

    public int getPort() {
        return mPort;
    }

    public void stop() {
        try {
            UnicastRemoteObject.unexportObject(mRegistry, true);
        } catch (NoSuchObjectException ignored) {
        }
    }

    private static Thread createOutputProcessor(String outputProcessorName,
            InputStream inputStream,
            Consumer<String> consumer) {
        BufferedReader inputReader = new BufferedReader(new InputStreamReader(inputStream));
        Thread thread = new Thread(() -> inputReader.lines().forEach(consumer));
        thread.setName(outputProcessorName);
        thread.start();
        return thread;
    }

    /**
     * This will start a new JVM and connect to the new JVM RMI registry.
     * <p/>
     * The server will start looking for ports available for the {@link Registry} until a free one
     * is found. The port number will be returned.
     * If no ports are available, a {@link RemoteException} will be thrown.
     * @param basePort port number to start looking for available ports
     * @param limit number of ports to check. The last port to be checked will be (basePort + limit)
     */
    public static ServerMain forkAndStartServer(int basePort, int limit)
            throws IOException, InterruptedException {
        // We start a new VM by copying all the parameter that we received in the current VM.
        // We only remove the agentlib parameter since that could cause a port collision and avoid
        // the new VM from starting.
        RuntimeMXBean runtimeMxBean = ManagementFactory.getRuntimeMXBean();
        List<String> arguments = runtimeMxBean.getInputArguments().stream()
                .filter(arg -> !arg.contains("-agentlib")) // Filter agentlib to avoid conflicts
                .collect(Collectors.toList());

        Path javaPath = Paths.get(System.getProperty("java.home"), "bin", "java");
        String thisClassName = ServerMain.class.getName()
                .replace('.','/');

        List<String> cmd = new ArrayList<>();
        cmd.add(javaPath.toString());

        // Inherited arguments
        cmd.addAll(arguments);

        // Classpath
        cmd.add("-cp");
        cmd.add(System.getProperty("java.class.path"));

        // Class name and path
        cmd.add(thisClassName);

        // ServerMain parameters [basePort. limit]
        cmd.add(Integer.toString(basePort));
        cmd.add(Integer.toString(limit));

        Process process = new ProcessBuilder()
                .command(cmd)
                .start();

        BlockingQueue<String> outputQueue = new ArrayBlockingQueue<>(10);
        Thread outputThread = createOutputProcessor("output", process.getInputStream(),
                outputQueue::offer);
        Thread errorThread = createOutputProcessor("error", process.getErrorStream(),
                System.err::println);

        Runnable killServer = () -> {
            process.destroyForcibly();
            outputThread.interrupt();
            errorThread.interrupt();
            try {
                outputThread.join();
            } catch (InterruptedException ignore) {
            }

            try {
                errorThread.join();
            } catch (InterruptedException ignore) {
            }
        };

        // Try to read the "Running on port" line in 10 lines. If it's not there just fail.
        for (int i = 0; i < 10; i++) {
            String line = outputQueue.poll(5, TimeUnit.SECONDS);

            if (line != null && line.startsWith(RUNNING_SERVER_STR)) {
                int runningPort = Integer.parseInt(line.substring(RUNNING_SERVER_STR.length()));
                System.out.println("Running on port " + runningPort);

                // We already know where the server is running so we just need to get the registry
                // and return our own instance of ServerMain
                Registry registry = LocateRegistry.getRegistry(runningPort);
                return new ServerMain(runningPort, registry) {
                    @Override
                    public void stop() {
                        killServer.run();
                    }
                };
            }
        }

        killServer.run();
        throw new IOException("Unable to find start string");
    }

    /**
     * The server will start looking for ports available for the {@link Registry} until a free one
     * is found. The port number will be returned.
     * If no ports are available, a {@link RemoteException} will be thrown.
     * @param basePort port number to start looking for available ports
     * @param limit number of ports to check. The last port to be checked will be (basePort + limit)
     */
    private static ServerMain startServer(int basePort, int limit) throws RemoteException {
        RemoteBridgeImpl remoteBridge = new RemoteBridgeImpl();
        RemoteBridge stub = (RemoteBridge) UnicastRemoteObject.exportObject(remoteBridge, 0);

        RemoteException lastException = null;
        for (int port = basePort; port <= basePort + limit; port++) {
            try {
                Registry registry = LocateRegistry.createRegistry(port);
                registry.rebind(RemoteBridge.class.getName(), stub);
                return new ServerMain(port, registry);
            } catch (RemoteException e) {
                lastException = e;
            }
        }

        if (lastException == null) {
            lastException = new RemoteException("Unable to start server");
        }

        throw lastException;
    }

    /**
     * Starts an RMI server that runs in the current JVM. Only for debugging.
     */
    public static ServerMain startLocalJvmServer() throws RemoteException {
        System.err.println("Starting server in the local JVM");
        return startServer(REGISTRY_BASE_PORT, 10);
    }

    public static void main(String[] args) throws RemoteException {
        int basePort = args.length > 0 ? Integer.parseInt(args[0]) : REGISTRY_BASE_PORT;
        int limit = args.length > 1 ? Integer.parseInt(args[1]) : 10;

        ServerMain server = startServer(basePort, limit);
        System.out.println(RUNNING_SERVER_STR + server.getPort());
    }
}