1 /* 2 * Copyright (C) 2017 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package com.android.layoutlib.bridge.remote.server; 18 19 import com.android.layout.remote.api.RemoteBridge; 20 import com.android.tools.layoutlib.annotations.NotNull; 21 22 import java.io.BufferedReader; 23 import java.io.IOException; 24 import java.io.InputStream; 25 import java.io.InputStreamReader; 26 import java.lang.management.ManagementFactory; 27 import java.lang.management.RuntimeMXBean; 28 import java.nio.file.Path; 29 import java.nio.file.Paths; 30 import java.rmi.NoSuchObjectException; 31 import java.rmi.RemoteException; 32 import java.rmi.registry.LocateRegistry; 33 import java.rmi.registry.Registry; 34 import java.rmi.server.UnicastRemoteObject; 35 import java.util.ArrayList; 36 import java.util.List; 37 import java.util.concurrent.ArrayBlockingQueue; 38 import java.util.concurrent.BlockingQueue; 39 import java.util.concurrent.TimeUnit; 40 import java.util.function.Consumer; 41 import java.util.stream.Collectors; 42 43 /** 44 * Main server class. The main method will start an RMI server for the {@link RemoteBridgeImpl} 45 * class. 46 */ 47 public class ServerMain { 48 private static final String RUNNING_SERVER_STR = "Server is running on port "; 49 public static int REGISTRY_BASE_PORT = 9000; 50 51 private final int mPort; 52 private final Registry mRegistry; 53 ServerMain(int port, @NotNull Registry registry)54 private ServerMain(int port, @NotNull Registry registry) { 55 mPort = port; 56 mRegistry = registry; 57 } 58 getPort()59 public int getPort() { 60 return mPort; 61 } 62 stop()63 public void stop() { 64 try { 65 UnicastRemoteObject.unexportObject(mRegistry, true); 66 } catch (NoSuchObjectException ignored) { 67 } 68 } 69 createOutputProcessor(String outputProcessorName, InputStream inputStream, Consumer<String> consumer)70 private static Thread createOutputProcessor(String outputProcessorName, 71 InputStream inputStream, 72 Consumer<String> consumer) { 73 BufferedReader inputReader = new BufferedReader(new InputStreamReader(inputStream)); 74 Thread thread = new Thread(() -> inputReader.lines().forEach(consumer)); 75 thread.setName(outputProcessorName); 76 thread.start(); 77 return thread; 78 } 79 80 /** 81 * This will start a new JVM and connect to the new JVM RMI registry. 82 * <p/> 83 * The server will start looking for ports available for the {@link Registry} until a free one 84 * is found. The port number will be returned. 85 * If no ports are available, a {@link RemoteException} will be thrown. 86 * @param basePort port number to start looking for available ports 87 * @param limit number of ports to check. The last port to be checked will be (basePort + limit) 88 */ forkAndStartServer(int basePort, int limit)89 public static ServerMain forkAndStartServer(int basePort, int limit) 90 throws IOException, InterruptedException { 91 // We start a new VM by copying all the parameter that we received in the current VM. 92 // We only remove the agentlib parameter since that could cause a port collision and avoid 93 // the new VM from starting. 94 RuntimeMXBean runtimeMxBean = ManagementFactory.getRuntimeMXBean(); 95 List<String> arguments = runtimeMxBean.getInputArguments().stream() 96 .filter(arg -> !arg.contains("-agentlib")) // Filter agentlib to avoid conflicts 97 .collect(Collectors.toList()); 98 99 Path javaPath = Paths.get(System.getProperty("java.home"), "bin", "java"); 100 String thisClassName = ServerMain.class.getName() 101 .replace('.','/'); 102 103 List<String> cmd = new ArrayList<>(); 104 cmd.add(javaPath.toString()); 105 106 // Inherited arguments 107 cmd.addAll(arguments); 108 109 // Classpath 110 cmd.add("-cp"); 111 cmd.add(System.getProperty("java.class.path")); 112 113 // Class name and path 114 cmd.add(thisClassName); 115 116 // ServerMain parameters [basePort. limit] 117 cmd.add(Integer.toString(basePort)); 118 cmd.add(Integer.toString(limit)); 119 120 Process process = new ProcessBuilder() 121 .command(cmd) 122 .start(); 123 124 BlockingQueue<String> outputQueue = new ArrayBlockingQueue<>(10); 125 Thread outputThread = createOutputProcessor("output", process.getInputStream(), 126 outputQueue::offer); 127 Thread errorThread = createOutputProcessor("error", process.getErrorStream(), 128 System.err::println); 129 130 Runnable killServer = () -> { 131 process.destroyForcibly(); 132 outputThread.interrupt(); 133 errorThread.interrupt(); 134 try { 135 outputThread.join(); 136 } catch (InterruptedException ignore) { 137 } 138 139 try { 140 errorThread.join(); 141 } catch (InterruptedException ignore) { 142 } 143 }; 144 145 // Try to read the "Running on port" line in 10 lines. If it's not there just fail. 146 for (int i = 0; i < 10; i++) { 147 String line = outputQueue.poll(5, TimeUnit.SECONDS); 148 149 if (line != null && line.startsWith(RUNNING_SERVER_STR)) { 150 int runningPort = Integer.parseInt(line.substring(RUNNING_SERVER_STR.length())); 151 System.out.println("Running on port " + runningPort); 152 153 // We already know where the server is running so we just need to get the registry 154 // and return our own instance of ServerMain 155 Registry registry = LocateRegistry.getRegistry(runningPort); 156 return new ServerMain(runningPort, registry) { 157 @Override 158 public void stop() { 159 killServer.run(); 160 } 161 }; 162 } 163 } 164 165 killServer.run(); 166 throw new IOException("Unable to find start string"); 167 } 168 169 /** 170 * The server will start looking for ports available for the {@link Registry} until a free one 171 * is found. The port number will be returned. 172 * If no ports are available, a {@link RemoteException} will be thrown. 173 * @param basePort port number to start looking for available ports 174 * @param limit number of ports to check. The last port to be checked will be (basePort + limit) 175 */ 176 private static ServerMain startServer(int basePort, int limit) throws RemoteException { 177 RemoteBridgeImpl remoteBridge = new RemoteBridgeImpl(); 178 RemoteBridge stub = (RemoteBridge) UnicastRemoteObject.exportObject(remoteBridge, 0); 179 180 RemoteException lastException = null; 181 for (int port = basePort; port <= basePort + limit; port++) { 182 try { 183 Registry registry = LocateRegistry.createRegistry(port); 184 registry.rebind(RemoteBridge.class.getName(), stub); 185 return new ServerMain(port, registry); 186 } catch (RemoteException e) { 187 lastException = e; 188 } 189 } 190 191 if (lastException == null) { 192 lastException = new RemoteException("Unable to start server"); 193 } 194 195 throw lastException; 196 } 197 198 /** 199 * Starts an RMI server that runs in the current JVM. Only for debugging. 200 */ 201 public static ServerMain startLocalJvmServer() throws RemoteException { 202 System.err.println("Starting server in the local JVM"); 203 return startServer(REGISTRY_BASE_PORT, 10); 204 } 205 206 public static void main(String[] args) throws RemoteException { 207 int basePort = args.length > 0 ? Integer.parseInt(args[0]) : REGISTRY_BASE_PORT; 208 int limit = args.length > 1 ? Integer.parseInt(args[1]) : 10; 209 210 ServerMain server = startServer(basePort, limit); 211 System.out.println(RUNNING_SERVER_STR + server.getPort()); 212 } 213 } 214