Revision created by MOE tool push_codebase.
[gae.git] / java / src / main / com / google / appengine / tools / KickStart.java
blob3ae416b419e2d04b53297f00e1657134e60b8f55
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 JACOCO_AGENT_JAR_FLAG = "--jacoco_agent_jar";
90 private static final String JACOCO_AGENT_JAR_ERROR_MESSAGE =
91 JACOCO_AGENT_JAR_FLAG + "=<path> expected.";
92 private static final String JACOCO_AGENT_ARGS_FLAG = "--jacoco_agent_args";
93 private static final String JACOCO_AGENT_ARGS_ERROR_MESSAGE =
94 JACOCO_AGENT_ARGS_FLAG + "=<jacoco agent args> expected.";
95 private static final String JACOCO_EXEC_FLAG = "--jacoco_exec";
96 private static final String JACOCO_EXEC_ERROR_MESSAGE =
97 JACOCO_EXEC_FLAG + "=<path> expected.";
99 private Process serverProcess = null;
101 public static void main(String[] args) {
102 new KickStart(args);
105 private KickStart(String[] args) {
106 String entryClass = null;
108 ProcessBuilder builder = new ProcessBuilder();
109 String home = System.getProperty("java.home");
110 String javaExe = home + File.separator + "bin" + File.separator + "java";
112 List<String> jvmArgs = new ArrayList<String>();
113 ArrayList<String> appServerArgs = new ArrayList<String>();
114 String jacocoAgentJarArg = null;
115 String jacocoAgentArgs = "";
116 String jacocoExecArg = "jacoco.exec";
118 List<String> command = builder.command();
119 command.add(javaExe);
121 boolean startOnFirstThread = System.getProperty("os.name").equalsIgnoreCase("Mac OS X");
122 String externalResourceDirArg = null;
123 boolean generateWar = false;
124 boolean noJavaAgent = false;
126 for (int i = 0; i < args.length; i++) {
127 if (args[i].startsWith(EXTERNAL_RESOURCE_DIR_FLAG)) {
128 externalResourceDirArg = extractValue(args[i], EXTERNAL_RESOURCE_DIR_ERROR_MESSAGE);
129 } else if (args[i].startsWith(GENERATED_WAR_DIR_FLAG)
130 || args[i].startsWith(GENERATE_WAR_FLAG)) {
131 generateWar = true;
132 } else if (args[i].startsWith(NO_JAVA_AGENT_FLAG)) {
133 noJavaAgent = true;
135 if (args[i].startsWith(JVM_FLAG)) {
136 jvmArgs.add(extractValue(args[i], JVM_FLAG_ERROR_MESSAGE));
137 } else if (args[i].startsWith(START_ON_FIRST_THREAD_FLAG)) {
138 startOnFirstThread =
139 Boolean.valueOf(extractValue(args[i], START_ON_FIRST_THREAD_ERROR_MESSAGE));
140 } else if (args[i].startsWith(JACOCO_AGENT_JAR_FLAG)) {
141 jacocoAgentJarArg = extractValue(args[i], JACOCO_AGENT_JAR_ERROR_MESSAGE);
142 } else if (args[i].startsWith(JACOCO_AGENT_ARGS_FLAG)) {
143 jacocoAgentArgs = extractValue(args[i], JACOCO_AGENT_ARGS_ERROR_MESSAGE);
144 } else if (args[i].startsWith(JACOCO_EXEC_FLAG)) {
145 jacocoExecArg = extractValue(args[i], JACOCO_EXEC_ERROR_MESSAGE);
146 } else if (entryClass == null) {
147 if (args[i].charAt(0) == '-') {
148 throw new IllegalArgumentException("Only --jvm_flag may precede classname, not "
149 + args[i]);
150 } else {
151 entryClass = args[i];
152 if (!entryClass.equals(DevAppServerMain.class.getName())) {
153 throw new IllegalArgumentException("KickStart only works for DevAppServerMain");
156 } else {
157 appServerArgs.add(args[i]);
161 if (entryClass == null) {
162 throw new IllegalArgumentException("missing entry classname");
165 if (externalResourceDirArg == null && generateWar) {
166 System.err.println(
167 "Generating a war directory requires " + "--" + EXTERNAL_RESOURCE_DIR_FLAG);
168 System.exit(1);
170 File newWorkingDir = newWorkingDir(externalResourceDirArg,
171 appServerArgs.toArray(new String[appServerArgs.size()]));
172 builder.directory(newWorkingDir);
174 if (startOnFirstThread) {
175 jvmArgs.add("-XstartOnFirstThread");
178 String classpath = System.getProperty("java.class.path");
179 StringBuilder newClassPath = new StringBuilder();
180 assert classpath != null : "classpath must not be null";
181 String[] paths = classpath.split(File.pathSeparator);
182 for (int i = 0; i < paths.length; ++i) {
183 newClassPath.append(new File(paths[i]).getAbsolutePath());
184 if (i != paths.length - 1) {
185 newClassPath.append(File.pathSeparator);
188 String sdkRoot = null;
189 String appDir = null;
190 List<String> absoluteAppServerArgs = new ArrayList<String>(appServerArgs.size());
192 for (int i = 0; i < appServerArgs.size(); ++i) {
193 String arg = appServerArgs.get(i);
194 if (arg.startsWith(SDK_ROOT_FLAG)) {
195 sdkRoot = new File(extractValue(arg, SDK_ROOT_ERROR_MESSAGE)).getAbsolutePath();
196 arg = SDK_ROOT_FLAG + "=" + sdkRoot;
197 } else if (arg.startsWith(EXTERNAL_RESOURCE_DIR_FLAG)) {
198 arg = EXTERNAL_RESOURCE_DIR_FLAG + "="
199 + new File(extractValue(arg, EXTERNAL_RESOURCE_DIR_ERROR_MESSAGE)).getAbsolutePath();
200 } else if (i == appServerArgs.size() - 1) {
201 if (!arg.startsWith("-")) {
202 File file = new File(arg);
203 if (file.exists()) {
204 arg = new File(arg).getAbsolutePath();
205 appDir = arg;
209 absoluteAppServerArgs.add(arg);
211 if (sdkRoot == null) {
212 sdkRoot = SdkInfo.getSdkRoot().getAbsolutePath();
214 boolean isVM = false;
215 if (appDir != null) {
216 isVM = isVMRuntime(appDir);
218 if (isVM) {
219 noJavaAgent = true;
220 jvmArgs.add("-D--enable_all_permissions=true");
222 if (!noJavaAgent) {
223 String agentJar = sdkRoot + "/lib/agent/appengine-agent.jar";
224 agentJar = agentJar.replace('/', File.separatorChar);
225 if (jacocoAgentJarArg != null) {
226 jvmArgs.add("-D--enable_all_permissions=true");
227 String jacocoAgentJar = new File(jacocoAgentJarArg).getAbsolutePath();
228 if (!jacocoAgentArgs.isEmpty()) {
229 jacocoAgentArgs = jacocoAgentArgs + ",";
231 jacocoAgentArgs = jacocoAgentArgs + "destfile=" + new File(jacocoExecArg).getAbsolutePath();
232 jvmArgs.add("-javaagent:" + jacocoAgentJar + "=" + jacocoAgentArgs);
233 if (newClassPath.length() > 0) {
234 newClassPath.append(File.pathSeparator);
236 newClassPath.append(agentJar);
237 } else {
238 jvmArgs.add("-javaagent:" + agentJar);
242 String jdkOverridesJar = sdkRoot + "/lib/override/appengine-dev-jdk-overrides.jar";
243 jdkOverridesJar = jdkOverridesJar.replace('/', File.separatorChar);
244 jvmArgs.add("-Xbootclasspath/p:" + jdkOverridesJar);
245 command.addAll(jvmArgs);
246 command.add("-classpath");
247 command.add(newClassPath.toString());
248 command.add(entryClass);
249 command.add("--property=kickstart.user.dir=" + System.getProperty("user.dir"));
250 if (isVM) {
251 command.add("--no_java_agent");
253 command.addAll(absoluteAppServerArgs);
255 logger.fine("Executing " + command);
256 System.out.println("Executing " + command);
258 Runtime.getRuntime().addShutdownHook(new Thread() {
259 @Override
260 public void run() {
261 if (serverProcess != null) {
262 serverProcess.destroy();
267 try {
268 serverProcess = builder.start();
269 } catch (IOException e) {
270 throw new RuntimeException("Unable to start the process", e);
273 new Thread(new OutputPump(serverProcess.getInputStream(),
274 new PrintWriter(System.out, true))).start();
275 new Thread(new OutputPump(serverProcess.getErrorStream(),
276 new PrintWriter(System.err, true))).start();
278 try {
279 serverProcess.waitFor();
280 } catch (InterruptedException e) {
283 serverProcess.destroy();
284 serverProcess = null;
287 private static String extractValue(String argument, String errorMessage) {
288 int indexOfEqualSign = argument.indexOf('=');
289 if (indexOfEqualSign == -1) {
290 throw new IllegalArgumentException(errorMessage);
292 return argument.substring(argument.indexOf('=') + 1);
296 * Encapsulates the logic to determine the working directory that should be set for the dev
297 * appserver process. If one is explicitly specified it will be used. Otherwise the last
298 * command-line argument will be used.
300 * @param workingDirectoryArg An explicitly specified path. If not {@code null} then it will be
301 * used.
302 * @param args The command-line arguments. If {@code workingDirectory} is {@code null} then the
303 * last command-line argument will be used as the working directory.
304 * @return The working directory to use. If the path to an existing directory was not specified
305 * then we exist with a failure.
307 private static File newWorkingDir(String workingDirectoryArg, String[] args) {
308 File workingDir;
309 if (workingDirectoryArg != null) {
310 workingDir = new File(workingDirectoryArg);
311 if (!workingDir.isDirectory()) {
312 System.err.println(workingDirectoryArg + " is not an existing directory.");
313 System.exit(1);
315 } else {
316 if (args.length < 1 || args[args.length - 1].startsWith("-")) {
317 new DevAppServerMain().printHelp(System.out);
318 System.exit(1);
320 workingDir = new File(args[args.length - 1]);
321 new DevAppServerMain().validateWarPath(workingDir);
323 return workingDir;
326 static boolean isVMRuntime(String appDir) {
327 File f = new File(appDir, "WEB-INF/appengine-web.xml");
328 if (!f.exists()) {
329 return false;
332 try {
333 AppEngineWebXmlReader reader = new AppEngineWebXmlReader(appDir);
334 AppEngineWebXml appEngineWebXml = reader.readAppEngineWebXml();
335 return appEngineWebXml.getUseVm();
336 } catch (AppEngineConfigException e) {
337 System.err.println("Error reading: " + f.getAbsolutePath());
338 return false;