App Engine Java SDK version 1.9.25
[gae.git] / java / src / main / com / google / appengine / tools / KickStart.java
blobed76ae46d8a5b6833ccfeb4fda7f945e617bda5b
1 // Copyright 2008 Google Inc. All Rights Reserved.
3 package com.google.appengine.tools;
5 import com.google.appengine.tools.admin.OutputPump;
6 import com.google.appengine.tools.development.DevAppServerMain;
7 import com.google.appengine.tools.info.SdkInfo;
8 import com.google.apphosting.utils.config.AppEngineConfigException;
9 import com.google.apphosting.utils.config.AppEngineWebXml;
10 import com.google.apphosting.utils.config.AppEngineWebXmlReader;
12 import java.io.File;
13 import java.io.IOException;
14 import java.io.PrintWriter;
15 import java.util.ArrayList;
16 import java.util.List;
17 import java.util.logging.Logger;
19 /**
20 * Launches a process in an operating-system agnostic way. Helps us avoid
21 * idiosyncrasies in scripts for different platforms. Currently this only
22 * works for DevAppServerMain.
24 * Takes a command line invocation like:
26 * <pre>
27 * java -cp ../lib/appengine-tools-api.jar com.google.appengine.tools.KickStart \
28 * --jvm_flag="-Dlog4j.configuration=log4j.props"
29 * com.google.appengine.tools.development.DevAppServerMain \
30 * --jvm_flag="-agentlib:jdwp=transport=dt_socket,server=y,address=7000"
31 * --address=localhost --port=5005 appDir
32 * </pre>
34 * and turns it into:
36 * <pre>
37 * java -cp &lt;an_absolute_path&gt;/lib/appengine-tools-api.jar \
38 * -Dlog4j.configuration=log4j.props \
39 * -agentlib:jdwp=transport=dt_socket,server=y,address=7000 \
40 * com.google.appengine.tools.development.DevAppServerMain \
41 * --address=localhost --port=5005 &lt;an_absolute_path&gt;/appDir
42 * </pre>
44 * while also setting its working directory (if appropriate).
45 * <p>
46 * All arguments between {@code com.google.appengine.tools.KickStart} and
47 * {@code com.google.appengine.tools.development.DevAppServerMain}, as well as
48 * all {@code --jvm_flag} arguments after {@code DevAppServerMain}, are consumed
49 * by KickStart. The remaining options after {@code DevAppServerMain} are
50 * given as arguments to DevAppServerMain, without interpretation by
51 * KickStart.
53 * At present, the only valid option to KickStart itself is:
54 * <DL>
55 * <DT>--jvm_flag=&lt;vm_arg&gt;</DT><DD>Passes &lt;vm_arg&gt; as a JVM
56 * argument for the child JVM. May be repeated.</DD>
57 * </DL>
58 * Additionally, if the --external_resource_dir argument is specified, we use it
59 * to set the working directory instead of the application war directory.
62 public class KickStart {
64 private static final Logger logger = Logger.getLogger(KickStart.class.getName());
66 private static final String EXTERNAL_RESOURCE_DIR_FLAG =
67 "--" + DevAppServerMain.EXTERNAL_RESOURCE_DIR_ARG;
68 private static final String EXTERNAL_RESOURCE_DIR_ERROR_MESSAGE =
69 EXTERNAL_RESOURCE_DIR_FLAG + "=<path> expected.";
71 private static final String GENERATE_WAR_FLAG = "--" + DevAppServerMain.GENERATE_WAR_ARG;
73 private static final String GENERATED_WAR_DIR_FLAG =
74 "--" + DevAppServerMain.GENERATED_WAR_DIR_ARG;
76 private static final String NO_JAVA_AGENT_FLAG = "--no_java_agent";
78 private static final String JVM_FLAG = "--jvm_flag";
79 private static final String JVM_FLAG_ERROR_MESSAGE =
80 JVM_FLAG + "=<flag> expected.\n" + JVM_FLAG + " may be repeated to supply multiple flags";
82 private static final String START_ON_FIRST_THREAD_FLAG = "--startOnFirstThread";
83 private static final String START_ON_FIRST_THREAD_ERROR_MESSAGE =
84 START_ON_FIRST_THREAD_FLAG + "=<boolean> expected";
86 private static final String SDK_ROOT_FLAG = "--sdk_root";
87 private static final String SDK_ROOT_ERROR_MESSAGE = SDK_ROOT_FLAG + "=<path> expected";
89 private static final String ENABLE_JACOCO_FLAG = "--enable_jacoco";
90 private static final String ENABLE_JACOCO_ERROR_MESSAGE =
91 ENABLE_JACOCO_FLAG + "=true|false expected.";
92 private static final String JACOCO_AGENT_JAR_FLAG = "--jacoco_agent_jar";
93 private static final String JACOCO_AGENT_JAR_ERROR_MESSAGE =
94 JACOCO_AGENT_JAR_FLAG + "=<path> expected.";
95 private static final String JACOCO_AGENT_ARGS_FLAG = "--jacoco_agent_args";
96 private static final String JACOCO_AGENT_ARGS_ERROR_MESSAGE =
97 JACOCO_AGENT_ARGS_FLAG + "=<jacoco agent args> expected.";
98 private static final String JACOCO_EXEC_FLAG = "--jacoco_exec";
99 private static final String JACOCO_EXEC_ERROR_MESSAGE =
100 JACOCO_EXEC_FLAG + "=<path> expected.";
102 private Process serverProcess = null;
104 public static void main(String[] args) {
105 new KickStart(args);
108 private KickStart(String[] args) {
109 String entryClass = null;
111 ProcessBuilder builder = new ProcessBuilder();
112 String home = System.getProperty("java.home");
113 String javaExe = home + File.separator + "bin" + File.separator + "java";
115 List<String> jvmArgs = new ArrayList<String>();
116 ArrayList<String> appServerArgs = new ArrayList<String>();
117 boolean enableJacoco = false;
118 String jacocoAgentJarArg = null;
119 String jacocoAgentArgs = "";
120 String jacocoExecArg = "jacoco.exec";
122 List<String> command = builder.command();
123 command.add(javaExe);
125 boolean startOnFirstThread = System.getProperty("os.name").equalsIgnoreCase("Mac OS X");
126 String externalResourceDirArg = null;
127 boolean generateWar = false;
128 boolean noJavaAgent = false;
130 for (int i = 0; i < args.length; i++) {
131 if (args[i].startsWith(EXTERNAL_RESOURCE_DIR_FLAG)) {
132 externalResourceDirArg = extractValue(args[i], EXTERNAL_RESOURCE_DIR_ERROR_MESSAGE);
133 } else if (args[i].startsWith(GENERATED_WAR_DIR_FLAG)
134 || args[i].startsWith(GENERATE_WAR_FLAG)) {
135 generateWar = true;
136 } else if (args[i].startsWith(NO_JAVA_AGENT_FLAG)) {
137 noJavaAgent = true;
139 if (args[i].startsWith(JVM_FLAG)) {
140 jvmArgs.add(extractValue(args[i], JVM_FLAG_ERROR_MESSAGE));
141 } else if (args[i].startsWith(START_ON_FIRST_THREAD_FLAG)) {
142 startOnFirstThread =
143 Boolean.valueOf(extractValue(args[i], START_ON_FIRST_THREAD_ERROR_MESSAGE));
144 } else if (args[i].startsWith(ENABLE_JACOCO_FLAG)) {
145 enableJacoco = "true".equals(extractValue(args[i], ENABLE_JACOCO_ERROR_MESSAGE));
146 } else if (args[i].startsWith(JACOCO_AGENT_JAR_FLAG)) {
147 jacocoAgentJarArg = extractValue(args[i], JACOCO_AGENT_JAR_ERROR_MESSAGE);
148 } else if (args[i].startsWith(JACOCO_AGENT_ARGS_FLAG)) {
149 jacocoAgentArgs = extractValue(args[i], JACOCO_AGENT_ARGS_ERROR_MESSAGE);
150 } else if (args[i].startsWith(JACOCO_EXEC_FLAG)) {
151 jacocoExecArg = extractValue(args[i], JACOCO_EXEC_ERROR_MESSAGE);
152 } else if (entryClass == null) {
153 if (args[i].charAt(0) == '-') {
154 throw new IllegalArgumentException("Only --jvm_flag may precede classname, not "
155 + args[i]);
156 } else {
157 entryClass = args[i];
158 if (!entryClass.equals(DevAppServerMain.class.getName())) {
159 throw new IllegalArgumentException("KickStart only works for DevAppServerMain");
162 } else {
163 appServerArgs.add(args[i]);
167 if (entryClass == null) {
168 throw new IllegalArgumentException("missing entry classname");
171 if (externalResourceDirArg == null && generateWar) {
172 System.err.println(
173 "Generating a war directory requires " + "--" + EXTERNAL_RESOURCE_DIR_FLAG);
174 System.exit(1);
176 File newWorkingDir = newWorkingDir(externalResourceDirArg,
177 appServerArgs.toArray(new String[appServerArgs.size()]));
178 builder.directory(newWorkingDir);
180 if (startOnFirstThread) {
181 jvmArgs.add("-XstartOnFirstThread");
184 String classpath = System.getProperty("java.class.path");
185 StringBuilder newClassPath = new StringBuilder();
186 assert classpath != null : "classpath must not be null";
187 String[] paths = classpath.split(File.pathSeparator);
188 for (int i = 0; i < paths.length; ++i) {
189 newClassPath.append(new File(paths[i]).getAbsolutePath());
190 if (i != paths.length - 1) {
191 newClassPath.append(File.pathSeparator);
194 String sdkRoot = null;
195 String appDir = null;
196 List<String> absoluteAppServerArgs = new ArrayList<String>(appServerArgs.size());
198 for (int i = 0; i < appServerArgs.size(); ++i) {
199 String arg = appServerArgs.get(i);
200 if (arg.startsWith(SDK_ROOT_FLAG)) {
201 sdkRoot = new File(extractValue(arg, SDK_ROOT_ERROR_MESSAGE)).getAbsolutePath();
202 arg = SDK_ROOT_FLAG + "=" + sdkRoot;
203 } else if (arg.startsWith(EXTERNAL_RESOURCE_DIR_FLAG)) {
204 arg = EXTERNAL_RESOURCE_DIR_FLAG + "="
205 + new File(extractValue(arg, EXTERNAL_RESOURCE_DIR_ERROR_MESSAGE)).getAbsolutePath();
206 } else if (i == appServerArgs.size() - 1) {
207 if (!arg.startsWith("-")) {
208 File file = new File(arg);
209 if (file.exists()) {
210 arg = new File(arg).getAbsolutePath();
211 appDir = arg;
215 absoluteAppServerArgs.add(arg);
217 if (sdkRoot == null) {
218 sdkRoot = SdkInfo.getSdkRoot().getAbsolutePath();
220 boolean isVM = false;
221 if (appDir != null) {
222 isVM = isVMRuntime(appDir);
224 if (isVM) {
225 noJavaAgent = true;
226 jvmArgs.add("-D--enable_all_permissions=true");
228 if (!noJavaAgent) {
229 String agentJar = sdkRoot + "/lib/agent/appengine-agent.jar";
230 agentJar = agentJar.replace('/', File.separatorChar);
231 if (enableJacoco) {
232 jvmArgs.add("-D--enable_all_permissions=true");
233 String jacocoAgentJar = new File(jacocoAgentJarArg).getAbsolutePath();
234 if (!jacocoAgentArgs.isEmpty()) {
235 jacocoAgentArgs = jacocoAgentArgs + ",";
237 jacocoAgentArgs = jacocoAgentArgs + "destfile=" + new File(jacocoExecArg).getAbsolutePath();
238 jvmArgs.add("-javaagent:" + jacocoAgentJar + "=" + jacocoAgentArgs);
239 if (newClassPath.length() > 0) {
240 newClassPath.append(File.pathSeparator);
242 newClassPath.append(agentJar);
243 } else {
244 jvmArgs.add("-javaagent:" + agentJar);
248 String jdkOverridesJar = sdkRoot + "/lib/override/appengine-dev-jdk-overrides.jar";
249 jdkOverridesJar = jdkOverridesJar.replace('/', File.separatorChar);
250 jvmArgs.add("-Xbootclasspath/p:" + jdkOverridesJar);
251 command.addAll(jvmArgs);
252 command.add("-classpath");
253 command.add(newClassPath.toString());
254 command.add(entryClass);
255 command.add("--property=kickstart.user.dir=" + System.getProperty("user.dir"));
256 if (isVM) {
257 command.add("--no_java_agent");
259 command.addAll(absoluteAppServerArgs);
261 logger.fine("Executing " + command);
262 System.out.println("Executing " + command);
264 Runtime.getRuntime().addShutdownHook(new Thread() {
265 @Override
266 public void run() {
267 if (serverProcess != null) {
268 serverProcess.destroy();
273 try {
274 serverProcess = builder.start();
275 } catch (IOException e) {
276 throw new RuntimeException("Unable to start the process", e);
279 new Thread(new OutputPump(serverProcess.getInputStream(),
280 new PrintWriter(System.out, true))).start();
281 new Thread(new OutputPump(serverProcess.getErrorStream(),
282 new PrintWriter(System.err, true))).start();
284 try {
285 serverProcess.waitFor();
286 } catch (InterruptedException e) {
289 serverProcess.destroy();
290 serverProcess = null;
293 private static String extractValue(String argument, String errorMessage) {
294 int indexOfEqualSign = argument.indexOf('=');
295 if (indexOfEqualSign == -1) {
296 throw new IllegalArgumentException(errorMessage);
298 return argument.substring(argument.indexOf('=') + 1);
302 * Encapsulates the logic to determine the working directory that should be set for the dev
303 * appserver process. If one is explicitly specified it will be used. Otherwise the last
304 * command-line argument will be used.
306 * @param workingDirectoryArg An explicitly specified path. If not {@code null} then it will be
307 * used.
308 * @param args The command-line arguments. If {@code workingDirectory} is {@code null} then the
309 * last command-line argument will be used as the working directory.
310 * @return The working directory to use. If the path to an existing directory was not specified
311 * then we exist with a failure.
313 private static File newWorkingDir(String workingDirectoryArg, String[] args) {
314 File workingDir;
315 if (workingDirectoryArg != null) {
316 workingDir = new File(workingDirectoryArg);
317 if (!workingDir.isDirectory()) {
318 System.err.println(workingDirectoryArg + " is not an existing directory.");
319 System.exit(1);
321 } else {
322 if (args.length < 1 || args[args.length - 1].startsWith("-")) {
323 new DevAppServerMain().printHelp(System.out);
324 System.exit(1);
326 workingDir = new File(args[args.length - 1]);
327 new DevAppServerMain().validateWarPath(workingDir);
329 return workingDir;
333 * Determine if the appDir contains a VM module or not.
334 * If the directory is an EAR directory, if 1 of the modules is a Managed VM, we treat
335 * the entire application as a collection of Managed VMs.
337 static boolean isVMRuntime(String appDir) {
338 if (appDir == null) {
339 return false;
341 File appengineWeb = new File(appDir, "WEB-INF/appengine-web.xml");
343 if (!appengineWeb.exists()) {
344 File ear = new File(appDir);
345 if (!ear.exists()) {
346 System.err.println("Application does not exist: " + ear.getAbsolutePath());
347 return false;
349 boolean isVM = false;
350 for (File war : ear.listFiles()) {
351 if (new File(war, "WEB-INF/appengine-web.xml").exists()) {
352 try {
353 AppEngineWebXmlReader reader = new AppEngineWebXmlReader(war.getAbsolutePath());
354 AppEngineWebXml appEngineWebXml = reader.readAppEngineWebXml();
355 if (appEngineWebXml.getUseVm() || appEngineWebXml.getEnv().equals("2")) {
356 isVM = true;
358 } catch (AppEngineConfigException e) {
359 System.err.println("Error reading module: " + war.getAbsolutePath());
363 return isVM;
366 try {
367 AppEngineWebXmlReader reader = new AppEngineWebXmlReader(appDir);
368 AppEngineWebXml appEngineWebXml = reader.readAppEngineWebXml();
369 return appEngineWebXml.getUseVm();
370 } catch (AppEngineConfigException e) {
371 System.err.println("Error reading module: " + appengineWeb.getAbsolutePath());
372 return false;