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
;
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
;
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:
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
37 * java -cp <an_absolute_path>/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 <an_absolute_path>/appDir
44 * while also setting its working directory (if appropriate).
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
53 * At present, the only valid option to KickStart itself is:
55 * <DT>--jvm_flag=<vm_arg></DT><DD>Passes <vm_arg> as a JVM
56 * argument for the child JVM. May be repeated.</DD>
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
) {
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
)) {
136 } else if (args
[i
].startsWith(NO_JAVA_AGENT_FLAG
)) {
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
)) {
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 "
157 entryClass
= args
[i
];
158 if (!entryClass
.equals(DevAppServerMain
.class.getName())) {
159 throw new IllegalArgumentException("KickStart only works for DevAppServerMain");
163 appServerArgs
.add(args
[i
]);
167 if (entryClass
== null) {
168 throw new IllegalArgumentException("missing entry classname");
171 if (externalResourceDirArg
== null && generateWar
) {
173 "Generating a war directory requires " + "--" + EXTERNAL_RESOURCE_DIR_FLAG
);
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
);
210 arg
= new File(arg
).getAbsolutePath();
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
);
226 jvmArgs
.add("-D--enable_all_permissions=true");
229 String agentJar
= sdkRoot
+ "/lib/agent/appengine-agent.jar";
230 agentJar
= agentJar
.replace('/', File
.separatorChar
);
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
);
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"));
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() {
267 if (serverProcess
!= null) {
268 serverProcess
.destroy();
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();
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
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
) {
315 if (workingDirectoryArg
!= null) {
316 workingDir
= new File(workingDirectoryArg
);
317 if (!workingDir
.isDirectory()) {
318 System
.err
.println(workingDirectoryArg
+ " is not an existing directory.");
322 if (args
.length
< 1 || args
[args
.length
- 1].startsWith("-")) {
323 new DevAppServerMain().printHelp(System
.out
);
326 workingDir
= new File(args
[args
.length
- 1]);
327 new DevAppServerMain().validateWarPath(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) {
341 File appengineWeb
= new File(appDir
, "WEB-INF/appengine-web.xml");
343 if (!appengineWeb
.exists()) {
344 File ear
= new File(appDir
);
346 System
.err
.println("Application does not exist: " + ear
.getAbsolutePath());
349 boolean isVM
= false;
350 for (File war
: ear
.listFiles()) {
351 if (new File(war
, "WEB-INF/appengine-web.xml").exists()) {
353 AppEngineWebXmlReader reader
= new AppEngineWebXmlReader(war
.getAbsolutePath());
354 AppEngineWebXml appEngineWebXml
= reader
.readAppEngineWebXml();
355 if (appEngineWebXml
.getUseVm() || appEngineWebXml
.getEnv().equals("2")) {
358 } catch (AppEngineConfigException e
) {
359 System
.err
.println("Error reading module: " + war
.getAbsolutePath());
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());